Dynamically Changing Xamarin.Forms Tab Icons When Selected
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 correctly, 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, 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.