James Montemagno
James Montemagno

Live, Love, Bike, and Code.

Tags


Twitter


Dynamically Changing Xamarin.Forms Tab Icons When Selected

James MontemagnoJames Montemagno

Spicing up your Xamarin.Forms tab can easily be done in a few ways. You can add tint color in Android when the user deselects a tab, which can also be done in iOS in addition to a full swap of a selected image. It was recently pointed out to me that these blogs highlighted a way of adding back a bit of nativeness, but didn't answer a different question of how to actually completely change the icon itself when a tab is selected. This question caught me off guard as I have never really changed the actual icon, but sure why not I said! My blog post on iOS actually gives you a way to do this if you label things correct, but that seems a bit maintainable and not cross-platform. So, today lets change those tab icons!

For this blog we are going to start with a blank Xamarin.Forms application for iOS and Android. Then we will add in a new TabbedPage that contains two tabs inside of it that will change icons when selected or unselected. We will be using some MVVM and Data Binding and we will be using my MVVM Helpers Library, so make sure you go install that from NuGet.

Get Some Icons

For this sample we will have two tabs and when we select either of them it will change to a specific icon and back to the original when deselected. So, go ahead and grab some icons from Glyphish for iOS and Android Asset Studio for Android. Make sure that the names are exactly the same and that you add them to the iOS/Android specific projects.

I have imported the following names into Android/iOS:

  • tab_target.png
  • tab_chat.png
  • tab_graph.png

IIconChange Interface

Before we setup any Views or ViewModels we want to create a simple interface that any of our ViewModels can implement that will tell us if the Page that the ViewModel is bound to is selected and what the current icon should be:

public interface IIconChange
{
    bool IsSelected { get; set; }
    string CurrentIcon { get; }
}

ViewModels

This small app that we will make will have two tabs, so let's create two ViewModels and implement the new IIconChange interface. The key is that when ever we change IsSelected we will raise a change notification for CurrentIcon, which we will return a string to the Icon we wanted for the current state.

Tab1ViewModel.cs

public class Tab1ViewModel : BaseViewModel, IIconChange
{
    public Tab1ViewModel()
    {
        Title = "Tab1";
    }

    bool isSelected;
    public bool IsSelected
    {
        get => isSelected;
        set
        {
            if (SetProperty(ref isSelected, value))
                OnPropertyChanged(nameof(CurrentIcon));
        }
    }
    public string CurrentIcon
    {
        get => IsSelected ? "tab_target.png" : "tab_chat.png";
    }
}

Tab2ViewModel.cs

public class Tab2ViewModel : BaseViewModel, IIconChange
{
    public Tab2ViewModel()
    {
        Title = "Tab2";
    }

    bool isSelected;
    public bool IsSelected 
    {
        get => isSelected;
        set
        {
            if (SetProperty(ref isSelected, value))
                OnPropertyChanged(nameof(CurrentIcon));
        }
    }
    public string CurrentIcon 
    {
        get => IsSelected ? "tab_target.png" : "tab_graph.png";
    }
}

BaseViewModel comes from that MVVM Helpers NuGet that I mentioned earlier and includes a lot of bindable properties such as Title.

Creating The Tabs

Before we create the TabbedPage let's create some pages for our ViewModels that we just created. I created two ContentPage XAML pages and simply data bound the Title and Icon property on the page. I named my pages TabIconsPage and TabIconsPage2.

XAML Example:

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
    xmlns:local="clr-namespace:TabIcons" 
    x:Class="TabIcons.TabIconsPage"
    Title="{Binding Title}"
    Icon="{Binding CurrentIcon}">
</ContentPage>

Setting Up MyTabs

Here we go! Finally time to setup and create the Tabs and set our Binding Context. The key here is that we will register for changes to the current page with CurrentPageChanged and then update the IsSelected property and trigger an event that anyone could loop into (more on that later).

public class MyTabs : TabbedPage
{
    //always save a reference to the current page
    Page currentPage;
    public MyTabs()
    {
        //create the pages and set the view models
        //you could also do this in the page code behind
        Children.Add(new TabIconsPage
        {
            BindingContext = new Tab1ViewModel
            {
                IsSelected = true
            }
        });
        Children.Add(new TabIconsPage2
        {
            BindingContext = new Tab2ViewModel()
        });

        currentPage = Children[0];

        //Register for page changes
        this.CurrentPageChanged += Handle_CurrentPageChanged;
    }

    //Update the IsSelected state and trigger an Event that anyone can loop into.
    public event EventHandler UpdateIcons;
    void Handle_CurrentPageChanged(object sender, EventArgs e)
    {
        var currentBinding = currentPage.BindingContext as IIconChange;
        if (currentBinding != null)
            currentBinding.IsSelected = false;

        currentPage = CurrentPage;
        currentBinding = currentPage.BindingContext as IIconChange;
        if (currentBinding != null)
            currentBinding.IsSelected = true;

        UpdateIcons?.Invoke(this, EventArgs.Empty);
    }
}

iOS is DONE!

Yup, that is right, iOS is totally done at this point. Xamarin.Forms for iOS actually will automatically update the icon for us when we trigger this change, which is really awesome. Android... of course it doesn't :(, browsing through the source code for Xamarin.Forms I found that the only way Android updates the tab icon is when a page is added or removed. So time to dip into Android source code.

Custom Renderer for Android

This code can easily be and should be combined with my previous entry on setting the tint color based on selection of the tab for Android. It is very similar, but this time we will not only save out our current TabLayout, but we will also register for our MyTabs event of UpdateIcons to actually update the icons:

using System;
using System.IO;
using System.Linq;
using Android.Support.Design.Widget;
using TabIcons;
using TabIcons.Droid;
using TabIcons.Interfaces;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Xamarin.Forms.Platform.Android.AppCompat;
[assembly: ExportRenderer(typeof(MyTabs), typeof(MyTabsRenderer))]
namespace TabIcons.Droid
{
    public class MyTabsRenderer : TabbedPageRenderer
    {
        TabLayout layout;
        protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e)
        {
            base.OnElementChanged(e);
            if(Element != null)
            {
                ((MyTabs)Element).UpdateIcons += Handle_UpdateIcons;
            }
        }

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


            if (layout == null && e.PropertyName == "Renderer")
            {
                layout = (TabLayout)ViewGroup.GetChildAt(1);
            }
        }

        void Handle_UpdateIcons(object sender, EventArgs e)
        {
            TabLayout tabs = layout;

            if (tabs == null)
                return;

            for (var i = 0; i < Element.Children.Count; i++)
            {
                var child = Element.Children[i].BindingContext as IIconChange;
                var icon = child.CurrentIcon;
                if (string.IsNullOrEmpty(icon))
                    continue;

                TabLayout.Tab tab = tabs.GetTabAt(i);
                SetCurrentTabIcon(tab, icon);
            }
        }

        void SetCurrentTabIcon(TabLayout.Tab tab, string icon)
        {
            tab.SetIcon(IdFromTitle(icon, ResourceManager.DrawableClass));
        }

        int IdFromTitle(string title, Type type)
        {
            string name = Path.GetFileNameWithoutExtension(title);
            int id = GetId(type, name);
            return id;
        }

        int GetId(Type type, string memberName)
        {
            object value = type.GetFields().FirstOrDefault(p => p.Name == memberName)?.GetValue(type)
                ?? type.GetProperties().FirstOrDefault(p => p.Name == memberName)?.GetValue(type);
            if (value is int)
                return (int)value;
            return 0;
        }
    }
}

Overall, this code is pretty simple and actually steals some code from the Xamarin.Forms project. Whenever the UpdateIcons event is triggered we will simply loop through all of the tabs and find the matching drawable for the CurrentIcon property.

Grab the Code

Hopefully you have found this helpful in making your tabs extra fancy. You can grab the source code from my new GitHub repository that will contain this and all future code samples from the blog.

Live, Love, Bike, and Code

Comments