I'm Andrew Hoefling, and I work for FileOnQ as a Lead Software Engineer building mobile technologies for Government, Financial and First Responders using Xamarin. 

 

Xamarin.Forms: A Better Tabbed Page Renderer


Xamarin.Forms provides a simple control for adding tabs to any application but the standard implementation has some limitations. With a little knowledge of how iOS and Android work you can start creating beautiful Tabs in your Xamarin.Forms applications that support custom colors and custom text.

Just about every Xamarin.Forms project I start has some need for a TabbedPage and the team quickly outgrows the default implementation because the standard colors are very boring. They also don't always fit into the color guide that the designers have picked. 

Custom Color

Let's learn how to add custom icon colors and custom text color to really make our TabbedPage pop.

Define the API

Before we get started let's look at a standard TabbedPage implementation and determine what APIs we need.

Here is the example that is pulled right out of the Xamarin.Forms Docs

<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:local="clr-namespace:TabbedPageWithNavigationPage;assembly=TabbedPageWithNavigationPage"
            x:Class="TabbedPageWithNavigationPage.MainPage">
    <local:TodayPage />
    <NavigationPage Title="Schedule" Icon="schedule.png">
        <x:Arguments>
            <local:SchedulePage />
        </x:Arguments>
    </NavigationPage>
</TabbedPage>

Missing APIs

There are a handful of APIs that we need to fully control our TabbedPage

SelectedIconColor The color of the icon for the selected tab
UnselectedIconColor The default icon color for unselected tabs
SelectedTextColor The font color for the selected tab
UnselectedTextColor The Font Color for the unselected tab

Example Usage

Before we start writing our custom renderer and implementation let's look at what we are building. We have established the APIs that want to add to control both the Icon and the Font color for the text that appears below the icon. In our example below we have a special TabbedPage that is using a CustomTabbedPage which includes the new APIs that we defined above. The new APIs give us granular control for the colors depending on application state.

<c:CustomTabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    xmlns:local="clr-namespace:TabbedSample.Views"
                    xmlns:c="clr-namespace:TabbedSample.Controls"
                    Title="Tabbed Sample"
                    BarBackgroundColor="Green"
                    BarTextColor="White"
                    UnselectedIconColor="White"
                    SelectedIconColor="Red"
                    x:Class="TabbedSample.Views.SampleTabbedPage">
    <c:CustomTabbedPage.Children>
        <local:MainPage />
        <local:MainPage2 />
    </c:CustomTabbedPage.Children>
</c:CustomTabbedPage>

Now that we have an idea of what we are working towards, let's get started with our shared code.

Shared Code Control

I always put my custom controls in a folder called "Controls" that is at the root directory of my shared code.

Inside this folder create a CustomTabbedPage.cs and make sure it is just a standard C# file, as we are going to be extending the base TabbedPage functionality.

SelectedIconColor

public Color SelectedIconColor
{
    get { return (Color)GetValue(SelectedIconColorProperty); }
    set { SetValue(SelectedIconColorProperty, value); }
}

public static readonly BindableProperty SelectedIconColorProperty = BindableProperty.Create(
    nameof(SelectedItemProperty),
    typeof(Color),
    typeof(CustomTabbedPage),
    Color.White);

UnselectedIconColor

public Color UnselectedIconColor
{
    get { return (Color)GetValue(UnelectedIconColorProperty); }
    set { SetValue(UnelectedIconColorProperty, value); }
}

public static readonly BindableProperty UnelectedIconColorProperty = BindableProperty.Create(
    nameof(UnselectedIconColor),
    typeof(Color),
    typeof(CustomTabbedPage),
    Color.White);

SelectedTextColor

public Color SelectedTextColor
{
    get { return (Color)GetValue(SelectedTextColorProperty); }
    set { SetValue(SelectedTextColorProperty, value); }
}

public static readonly BindableProperty SelectedTextColorProperty = BindableProperty.Create(
    nameof(SelectedTextColor),
    typeof(Color),
    typeof(CustomTabbedPage),
    Color.White);

UnselectedTextColor

public Color UnselectedTextColor
{
    get { return (Color)GetValue(UnselectedTextColorProperty); }
    set { SetValue(UnselectedTextColorProperty, value); }
}

public static readonly BindableProperty UnselectedTextColorProperty = BindableProperty.Create(
    nameof(UnselectedTextColor),
    typeof(Color),
    typeof(CustomTabbedPage),
    Color.White);

Completed CustomTabbedPage

public class CustomTabbedPage : TabbedPage
{
    public Color SelectedIconColor
    {
        get { return (Color)GetValue(SelectedIconColorProperty); }
        set { SetValue(SelectedIconColorProperty, value); }
    }

    public static readonly BindableProperty SelectedIconColorProperty = BindableProperty.Create(
        nameof(SelectedItemProperty),
        typeof(Color),
        typeof(CustomTabbedPage),
        Color.White);

    public Color UnselectedIconColor
    {
        get { return (Color)GetValue(UnelectedIconColorProperty); }
        set { SetValue(UnelectedIconColorProperty, value); }
    }

    public static readonly BindableProperty UnelectedIconColorProperty = BindableProperty.Create(
        nameof(UnselectedIconColor),
        typeof(Color),
        typeof(CustomTabbedPage),
        Color.White);

    public Color SelectedTextColor
    {
        get { return (Color)GetValue(SelectedTextColorProperty); }
        set { SetValue(SelectedTextColorProperty, value); }
    }

    public static readonly BindableProperty SelectedTextColorProperty = BindableProperty.Create(
        nameof(SelectedTextColor),
        typeof(Color),
        typeof(CustomTabbedPage),
        Color.White);

    public Color UnselectedTextColor
    {
        get { return (Color)GetValue(UnselectedTextColorProperty); }
        set { SetValue(UnselectedTextColorProperty, value); }
    }

    public static readonly BindableProperty UnselectedTextColorProperty = BindableProperty.Create(
        nameof(UnselectedTextColor),
        typeof(Color),
        typeof(CustomTabbedPage),
        Color.White);
}

Android Renderer

Let's first look at building the custom renderer for the Android Platform. I prefer to put all of my custom renderer implementations in the Renderers folder.

Create an empty renderer class the inherits from the default renderer

public class CustomTabbedPageRenderer : TabbedPageRenderer
{
    public CustomTabbedPageRenderer(Context context) : base(context) { }
}

Default Colors

In my implementation I found it useful to create a set of default colors just in case the shared code doesn't specify a color. This will prevent any runtime errors happening on the Android platform.

  • Navigate to Resources->values->colors.xml

Add the following properties: 

Default Colors

<color name="tabBarUnselected">#80ffffff</color>
<color name="tabBarSelected">#ffffff</color>

colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="launcher_background">#FFFFFF</color>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>
    <color name="tabBarUnselected">#80ffffff</color>
    <color name="tabBarSelected">#ffffff</color>
</resources>

Android Implementation

The algorithm is as follows:

  1. Iterate through each tab
  2. Get the TabLayout.Tab.Icon for each tab
  3. Update the icon color based on the selected tab vs unselected tab value

Algorithm

for (int i = 0; i < _layout.TabCount; i++)
{
    var tab = _layout.GetTabAt(i);
    var icon = tab.Icon;
    if (icon != null)
    {
        var color = tab.IsSelected ? selectedColor : unselectedColor;
        icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
        icon.SetColorFilter(color, PorterDuff.Mode.SrcIn);
    }
}

Full Implementation

using Android.Content;
using Android.Graphics;
using Android.Support.Design.Widget;
using Android.Support.V4.Content;
using Android.Support.V4.View;
using Android.Views;
using System.ComponentModel;
using TabbedSample.Controls;
using TabbedSample.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms.Platform.Android.AppCompat;


[assembly: ExportRenderer(typeof(CustomTabbedPage), typeof(CustomTabbedPageRenderer))]
namespace TabbedSample.Droid.Renderers
{
    public class CustomTabbedPageRenderer : TabbedPageRenderer
    {
        private bool _isConfigured = false;
        private ViewPager _pager;
        private TabLayout _layout;

        public CustomTabbedPageRenderer(Context context) : base(context) { }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            _pager = (ViewPager)ViewGroup.GetChildAt(0);
            _layout = (TabLayout)ViewGroup.GetChildAt(1);

            var control = (CustomTabbedPage)sender;
            Android.Graphics.Color selectedColor;
            Android.Graphics.Color unselectedColor;
            if (control != null)
            {
                selectedColor = control.SelectedIconColor.ToAndroid();
                unselectedColor = control.UnselectedIconColor.ToAndroid();
            }
            else
            {
                selectedColor = new Android.Graphics.Color(ContextCompat.GetColor(Context, Resource.Color.tabBarSelected));
                unselectedColor = new Android.Graphics.Color(ContextCompat.GetColor(Context, Resource.Color.tabBarUnselected));
            }

            for (int i = 0; i < _layout.TabCount; i++)
            {
                var tab = _layout.GetTabAt(i);
                var icon = tab.Icon;
                if (icon != null)
                {
                    var color = tab.IsSelected ? selectedColor : unselectedColor;
                    icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
                    icon.SetColorFilter(color, PorterDuff.Mode.SrcIn);
                }
            }
        }
    }
}

iOS Renderer

I find the iOS code slightly easier to manage by the way the APIs are accessible. There is no additional resources or special default files you need to create like we did in Android. The algorithm is very similar (this is only for updating the text color)

  1. Iterate through each tab
  2. Get the TabBar.Items[i]
  3. Update the text color

The icon color is handled differently as there is an easy to use api that we call to update it.

Icon Color

TabBar.UnselectedItemTintColor = unselectedColor;
TabBar.SelectedImageTintColor = selectedColor;

UpdateItem

private void UpdateItem(UITabBarItem item, UIColor selected, UIColor unselected)
{
    if (item == null) return;

    item.SetTitleTextAttributes(new UITextAttributes
    {
        TextColor = selected
    }, UIControlState.Selected);

    item.SetTitleTextAttributes(new UITextAttributes
    {
        TextColor = unselected
    }, UIControlState.Normal);
}

Complete Renderer

public class CustomTabbedPageRenderer : TabbedRenderer
{
    public override void ViewWillAppear(bool animated)
    {
        if (TabBar?.Items == null) return;

        var control = (CustomTabbedPage)Element;
        UIColor selectedColor;
        UIColor unselectedColor;
        UIColor selectedTextColor;
        UIColor unselectedTextColor;
        if (control != null)
        {
            selectedColor = control.SelectedIconColor.ToUIColor();
            unselectedColor = control.UnselectedIconColor.ToUIColor();
            selectedTextColor = control.SelectedTextColor.ToUIColor();
            unselectedTextColor = control.UnselectedTextColor.ToUIColor();
        }
        else
        {
            selectedColor = UIColor.White;
            unselectedColor = UIColor.Black;
            selectedTextColor = UIColor.White;
            unselectedTextColor = UIColor.White;
        }

        TabBar.UnselectedItemTintColor = unselectedColor;
        TabBar.SelectedImageTintColor = selectedColor;


        var tabs = Element as TabbedPage;
        if (tabs != null)
        {
            for (int i = 0; i < TabBar.Items.Length; i++)
            {
                UpdateItem(TabBar.Items[i], selectedTextColor, unselectedTextColor);
            }
        }

        base.ViewWillAppear(animated);
    }

    public override void ItemSelected(UITabBar tabbar, UITabBarItem item)
    {
        var page = ((TabbedPage)Element).CurrentPage;

    }

    private void UpdateItem(UITabBarItem item, UIColor selected, UIColor unselected)
    {
        if (item == null) return;

        item.SetTitleTextAttributes(new UITextAttributes
        {
            TextColor = selected
        }, UIControlState.Selected);

        item.SetTitleTextAttributes(new UITextAttributes
        {
            TextColor = unselected
        }, UIControlState.Normal);
    }
}

Xamarin.Forms 4.0.0

If you are using the latest and greatest version of Xamarin.Forms you don't need to do this because it is now built into the platform.

 

 

 

-Happy Coding


Share

Tags

XamarinXamarin.FormsXAMLRendereriOSAndroid