"Pure" Xamarin.Forms tabs: bottom bar tabs, underlined tabs, custom tabs, egg and tabs, tabs bacon sausage and tabs

TABS! TABS! TABS! TABS! 1

We love tabs.
Since we learnt that hamburger menu was the master of UIvil, every one is embracing tabs for their app navigation. Android added bottom bar navigation in their supports libs years ago, and iOS apps stole the android tab layout.

Admit it: you can't get enough tabs.

Tabs

Here at sharpnado, we thrive at delivering top quality material for the community (I don't even know if this a proper english sentence, but it feels very professional).

So I present you Sharpnado Pure Xamarin Forms Tabs (aka SPXFT).
It's available in the v1.0 of the Sharpnado.Forms.Presentation nuget packages.

Source code can be found here:
https://github.com/roubachof/Sharpnado.Presentation.Forms

Why pure Xamarin Forms UI

The term "Pure Xamarin Forms" doesn't really mean much. What I mean by that is that there is no renderers, the tabs I will present you are only implemented with Xamarin.Forms views, so it makes them really easy to extend, animate, place anywhere, rotate, change color, change font, well you get the idea.

I saw that Xamarin was pushing Visual and Material theming to ease our pain, which is great! I love consistent UI and plaftorm specific UI.

Unfortunately (or not) most of the designers I worked with don't care about platform rules, they have a specific design in mind and they want the same design on both platforms. And as a developer, you end up implementing some crazy custom controls with gradients.

The tabs architecture I will now present you was conceived in this context: it let you really change anything about them, and even implement your own custom tabs easily.

We will cover two kinds of UI scenarios:

1. Top tabs UI

iOS Android

2. Bottom tabs UI

iOS Android

Architecture

Well I am a very big fan of composition (don't forget folks, composition vs inheritance), so I tried to isolate each responsability as much as I can.

You can find all the architecture code at: https://github.com/roubachof/Sharpnado.Presentation.Forms.

TabItem

The TabItem is a base abstract class for all tabs.
It has several bindable properties:

    public bool IsSelected
    {
        get => (bool)GetValue(IsSelectedProperty);
        set => SetValue(IsSelectedProperty, value);
    }

    public string Label
    {
        get => (string)GetValue(LabelProperty);
        set => SetValue(LabelProperty, value);
    }

    public double LabelSize
    {
        get => (double)GetValue(LabelSizeProperty);
        set => SetValue(LabelSizeProperty, value);
    }

    public Color UnselectedLabelColor
    {
        get => (Color)GetValue(UnselectedLabelColorProperty);
        set => SetValue(UnselectedLabelColorProperty, value);
    }

    public Color SelectedTabColor
    {
        get => (Color)GetValue(SelectedTabColorProperty);
        set => SetValue(SelectedTabColorProperty, value);
    }

    public string FontFamily
    {
        get => (string)GetValue(FontFamilyProperty);
        set => SetValue(FontFamilyProperty, value);
    }

Two concrete classes implement this abstract class:

1. UnderlinedTabItem

This is a classic "Android" tab, with text.
When the tab is selected, the text is underlined.

2. BottomTabItem

This is a classic "iOS" bottom tab.
It has an icon and is not underlined.

It adds several more bindable properties:

    public string IconImageSource
    {
        get => (string)GetValue(IconImageSourceProperty);
        set => SetValue(IconImageSourceProperty, value);
    }

    public double IconSize
    {
        get => (double)GetValue(IconSizeProperty);
        set => SetValue(IconSizeProperty, value);
    }

    public Color UnselectedIconColor
    {
        get => (Color)GetValue(UnselectedIconColorProperty);
        set => SetValue(UnselectedIconColorProperty, value);
    }

TabHostView

The TabHostView has the responsability of bringing tabs together in a horizontal layout, managing the states of the tabs (basically which one is selected).

    public int SelectedIndex
    {
        get => (int)GetValue(SelectedIndexProperty);
        set => SetValue(SelectedIndexProperty, value);
    }

    public ShadowType ShadowType
    {
        get => (ShadowType)GetValue(ShadowTypeProperty);
        set => SetValue(ShadowTypeProperty, value);
    }

ViewSwitcher

The ViewSwitcher selects the view linked to the matching tab.

You can see it as a stack of views, hiding the ones not selected.

Remark: you can totally use a ViewSwitcher without a TabHostView.

    public int SelectedIndex
    {
        get => (int)GetValue(SelectedIndexProperty);
        set => SetValue(SelectedIndexProperty, value);
    }

    public bool Animate { get; set; } = true;

Putting it all together

We'll see now two different examples of layout with the Silly! App (https://github.com/roubachof/Xamarin-Forms-Practices).

UnderlinedTabItem with ViewSwitcher

Let's consider this view:

And let's have a look at its code:

<Grid Padding="{StaticResource StandardThickness}"  
      ColumnSpacing="0"
      RowSpacing="0">
    <Grid.RowDefinitions>
        <RowDefinition Height="200" />
        <RowDefinition Height="40" />
        <RowDefinition Height="30" />
        <RowDefinition Height="30" />
        <RowDefinition Height="50" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <!-- first 4 rows then... -->

    <tabs:TabHostView x:Name="TabHost"
                      Grid.Row="4"
                      Margin="-16,0"
                      BackgroundColor="White"
                      SelectedIndex="{Binding Source={x:Reference Switcher}, 
                                              Path=SelectedIndex, 
                                              Mode=TwoWay}"
                      ShadowType="Bottom">
        <tabs:UnderlinedTabItem Style="{StaticResource TabStyle}" 
                                Label="{loc:Translate Tabs_Quote}" />
        <tabs:UnderlinedTabItem Style="{StaticResource TabStyle}" 
                                Label="{loc:Translate Tabs_Filmography}" />
        <tabs:UnderlinedTabItem Style="{StaticResource TabStyle}" 
                                Label="{loc:Translate Tabs_Meme}" />
    </tabs:TabHostView>

    <ScrollView Grid.Row="5">
        <tabs:ViewSwitcher x:Name="Switcher"
                           Animate="True"
                           SelectedIndex="{Binding SelectedViewModelIndex, 
                                                   Mode=TwoWay}">
            <details:Quote Animate="True" BindingContext="{Binding Quote}" />
            <details:Filmo Animate="True" BindingContext="{Binding Filmo}" />
            <details:Meme Animate="True" BindingContext="{Binding Meme}" />
        </tabs:ViewSwitcher>
    </ScrollView>
</Grid>  

The TabHostView and the ViewSwitcher are really two independent components, and you can place them anywhere. They don't need to be next to each other (even if it would be weird I must admit).

Since they don't know each other, you just need to link them through their SelectedIndex property. You will bind the ViewSwitcher to your view model, and the TabHostView to the ViewSwitcher's SelectedIndex property.

You can see a ShadowType property. It adds a nice little shadow "à la Material" to bring you the nice and fancy elevation effect.
For top tabs, we want the shadow at the Bottom of our tabs.

You can also see a mysterious Animate property. It just adds a nice appearing effect. It's really just a little bonus.

View model

    public ViewModelLoader<SillyDudeVmo> SillyDudeLoader { get; }

    public QuoteVmo Quote { get; private set; }

    public FilmoVmo Filmo { get; private set; }

    public MemeVmo Meme { get; private set; }

    public int SelectedViewModelIndex
    {
        get => _selectedViewModelIndex;
        set => SetAndRaise(ref _selectedViewModelIndex, value);
    }

    public override void Load(object parameter)
    {
        SillyDudeLoader.Load(() => LoadSillyDude((int)parameter));
    }

    private async Task<SillyDudeVmo> LoadSillyDude(int id)
    {
        var dude = await _dudeService.GetSilly(id);

        Quote = new QuoteVmo(
            dude.SourceUrl,
            dude.Description,
            new TapCommand(url => Device.OpenUri(new Uri((string)url))));
        Filmo = new FilmoVmo(dude.FilmoMarkdown);
        Meme = new MemeVmo(dude.MemeUrl);
        RaisePropertyChanged(nameof(Quote));
        RaisePropertyChanged(nameof(Filmo));
        RaisePropertyChanged(nameof(Meme));

        return new SillyDudeVmo(dude, null);
    }

Well I won't go into details it's pretty obvious.
If you want to know more about the mystery ViewModelLoader, please read this post (https://www.sharpnado.com/taskloaderview-async-init-made-easy/).

Styling

The tab style is defined in the content page resources, but we could put it the App.xaml since most of the time we will have one type of top tabs (well it's up to your crazy designer really :)

<ContentPage.Resources>  
    <ResourceDictionary>
        <Style x:Key="TabStyle" TargetType="tabs:UnderlinedTabItem">
            <Setter Property="SelectedTabColor" Value="{StaticResource White}" />
            <Setter Property="FontFamily" Value="{StaticResource FontSemiBold}" />
            <Setter Property="LabelSize" Value="14" />
            <Setter Property="BackgroundColor" Value="{StaticResource Accent}" />
            <Setter Property="UnselectedLabelColor" Value="White" />
        </Style>
    </ResourceDictionary>
</ContentPage.Resources>  

BottomTabItem with ViewSwitcher

Now let's consider this view:

And let's have a look at its xaml:

<Grid ColumnSpacing="0" RowSpacing="0">  
    <Grid.RowDefinitions>
        <RowDefinition Height="{StaticResource ToolbarHeight}" />
        <RowDefinition Height="*" />
        <RowDefinition x:Name="BottomBarRowDefinition" 
                       Height="{StaticResource BottomBarHeight}" />
    </Grid.RowDefinitions>

    <tb:Toolbar Title="Silly App!"
                BackgroundColor="{StaticResource Accent}"
                ForegroundColor="White"
                HasShadow="True" />

    <tabs:ViewSwitcher x:Name="Switcher"
                       Grid.Row="1"
                       Animate="False"
                       SelectedIndex="{Binding SelectedViewModelIndex, 
                                               Mode=TwoWay}">
        <tabsLayout:HomeView BindingContext="{Binding HomePageViewModel}" />
        <tabsLayout:ListView BindingContext="{Binding ListPageViewModel}" />
        <tabsLayout:GridView BindingContext="{Binding GridPageViewModel}" />
    </tabs:ViewSwitcher>

    <tabs:TabHostView x:Name="TabHost"
                      Grid.Row="2"
                      BackgroundColor="White"
                      SelectedIndex="{Binding Source={x:Reference Switcher}, 
                                              Path=SelectedIndex, 
                                              Mode=TwoWay}"
                      ShadowType="Top">
        <tabs:BottomTabItem Style="{StaticResource BottomTabStyle}"
                            IconImageSource="house_96"
                            Label="{localization:Translate Tabs_Home}" />
        <tabs:BottomTabItem Style="{StaticResource BottomTabStyle}"
                            IconImageSource="list_96"
                            Label="{localization:Translate Tabs_List}" />
        <tabs:BottomTabItem Style="{StaticResource BottomTabStyle}"
                            IconImageSource="grid_view_96"
                            Label="{localization:Translate Tabs_Grid}" />
    </tabs:TabHostView>

</Grid>  

It's exactly the same thing as our top tabs sample, but we used BottomBarItem instead of UnderlinedTab. And our ViewSwitcher is standing above our TabHostView.
Simple.

Styling

<ContentPage.Resources>  
    <ResourceDictionary>
        <Style x:Key="BottomTabStyle" TargetType="tabs:BottomTabItem">
            <Setter Property="SelectedTabColor" Value="{StaticResource Accent}" />
            <Setter Property="UnselectedLabelColor" Value="Gray" />
            <Setter Property="UnselectedIconColor" Value="LightGray" />
            <Setter Property="FontFamily" Value="{StaticResource FontLight}" />
            <Setter Property="LabelSize" Value="14" />
            <Setter Property="IconSize" Value="28" />
        </Style>
    </ResourceDictionary>
</ContentPage.Resources>  

Custom SPAM tabs !

As I said, your designer can go cuckoo and you won't even sweat it.
Just extend the abstract TabItem and fulfill the wildest dreams of your colleagues.

<tabs:TabItem x:Class="SillyCompany.Mobile.Practices.Presentation.CustomViews.SpamTab"  
              xmlns="http://xamarin.com/schemas/2014/forms"
              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
              xmlns:tabs="clr-namespace:Sharpnado.Presentation.Forms.CustomViews.Tabs;assembly=Sharpnado.Presentation.Forms"
              x:Name="RootLayout">
    <ContentView.Content>
        <Grid ColumnSpacing="0" RowSpacing="0">
            <Image x:Name="Spam"
                   VerticalOptions="End"
                   Aspect="Fill"
                   Source="{Binding Source={x:Reference RootLayout}, 
                                    Path=SpamImage}" />
            <Image x:Name="Foot"
                   Aspect="Fill"
                   Source="monty_python_foot" />
        </Grid>
    </ContentView.Content>
</tabs:TabItem>

...

<tabs:TabHostView x:Name="TabHost"  
                  Grid.Row="2"
                  BackgroundColor="White"
                  SelectedIndex="{Binding Source={x:Reference Switcher}, 
                                          Path=SelectedIndex, 
                                          Mode=TwoWay}"
                  ShadowType="Top">
    <tb:SpamTab SpamImage="spam_classic_home" />
    <tb:SpamTab SpamImage="spam_classic_list" />
    <tb:SpamTab SpamImage="spam_classic_grid" />

...

Please don't be shy with Xamarin.Forms animations, it's so easy to use and so powerful thanks to the amazing C# Task api.

USE.
THEM.

private void Animate(bool isSelected)  
{
    double targetFootOpacity = isSelected ? 1 : 0;
    double targetFootTranslationY = isSelected ? 0 : -_height;
    double targetHeightSpam = isSelected ? 0 : _height;

    NotifyTask.Create(
        async () =>
        {
            Task fadeFootTask = Foot.FadeTo(
                targetFootOpacity, 500);
            Task translateFootTask = Foot.TranslateTo(
                0, targetFootTranslationY, 250, Easing.CubicOut);
            Task heightSpamTask = Spam.HeightRequestTo(
                targetHeightSpam, 250, Easing.CubicOut);

            await Task.WhenAll(fadeFootTask, translateFootTask, heightSpamTask);

            Spam.HeightRequest = targetHeightSpam;
            Foot.TranslationY = targetFootTranslationY;
            Foot.Opacity = targetFootOpacity;
        });
}