Bottom menu for Xamarin Forms (Android)

In this chapter you will know how to implement a custom TabbedPage renderer for Android to show tabs in the bottom. That’s pretty general requirement for many iOS+Android projects so let’s figure out how to make it working in the correct way.

First of all I do not want to create a very custom tabbed page in PCL and mimic the native behavior or make implementations for both platforms. I like how it works on iOS so we will leave it unchanged. The result will look like this:

In my example Android utilizes Xamarin Forms wrapper of the perfect BottomBar control by Iiro Krankka.

Let’s start with applying the material theme for Android application by deriving from FormsAppCompatActivity, defining the toolbar layout and the custom theme. This will let us to get rid of the tab bar being used by Xamarin for Android by default. In material approach we use toolbal instead of action bar so we will just disable action bar using custom theme.

[Activity(Label = "CustomBottomMenu", Icon = "@drawable/icon", MainLauncher = true,
          ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
          Theme = "@style/AppTheme")]
public class MainActivity : FormsAppCompatActivity
{
    protected override void OnCreate(Bundle bundle)
    {
        ToolbarResource = Resource.Layout.toolbar;

        base.OnCreate(bundle);

        Xamarin.Forms.Forms.Init(this, bundle);
        LoadApplication(new App());
    }
}

Put the following toolbar.axml in Resources/layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:minHeight="?attr/actionBarSize"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
    app:layout_scrollFlags="scroll|enterAlways" />
<?xml version="1.0" encoding="utf-8" ?>
<resources>
  <style name="AppTheme" parent="AppTheme.Base">
  </style>
  <style name="AppTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorPrimary">@color/primary</item>
    <item name="colorPrimaryDark">@color/primaryDark</item>
    <item name="colorAccent">@color/accent</item>
    <item name="windowActionModeOverlay">true</item>
  </style>
</resources>

I assume you have a couple of pages deriving from the base Xamarin’s ContentPage. Next in code we will need to call OnAppearing and OnDisappearing methods manually for our pages. So if you have a base class for all of your pages then add a pair of methods SendAppearing and SendDisappearing to it. Create such class if you don’t have it and implement these 2 methods:

public class BaseContentPage : ContentPage
{
    public void SendAppearing()
    {
        OnAppearing();
    }

    public void SendDisappearing()
    {
        OnDisappearing();
    }
}

MainPage will host our child pages:

<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:pages="clr-namespace:CustomBottomMenu.Pages;assembly=CustomBottomMenu"
             x:Class="CustomBottomMenu.Pages.MainPage">
  
  <pages:AboutPage Icon="ic_about" />
  <pages:ContactsPage Icon="ic_contact" />
  
</TabbedPage>
public partial class MainPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    protected override void OnCurrentPageChanged()
    {
        base.OnCurrentPageChanged();

        Title = CurrentPage?.Title;
    }
}

Now let’s look at the MainPageRenderer where the magic happens. I’ve left the most interesting methods here. See the full sources on GitHub.

internal class MainPageRenderer : VisualElementRenderer&amp;amp;lt;MainPage&amp;amp;gt;, IOnTabClickListener
{
    public MainPageRenderer()
    {
        // Required to say packager to not to add child pages automatically
        AutoPackage = false;
    }

    public void OnTabSelected(int position)
    {
        LoadPageContent(position);
    }

    protected override void OnElementChanged(ElementChangedEventArgs&amp;amp;lt;MainPage&amp;amp;gt; e)
    {
        base.OnElementChanged(e);

        if (e.OldElement != null)
        {
            ClearElement(e.OldElement);
        }

        if (e.NewElement != null)
        {
            InitializeElement(e.NewElement);
        }
    }

    protected override void OnLayout(bool changed, int l, int t, int r, int b)
    {
        if (Element == null)
        {
            return;
        }

        int width = r - l;
        int height = b - t;

        _bottomBar.Measure(
            MeasureSpec.MakeMeasureSpec(width, MeasureSpecMode.Exactly),
            MeasureSpec.MakeMeasureSpec(height, MeasureSpecMode.AtMost));

        // We need to call measure one more time with measured sizes
        // in order to layout the bottom bar properly
        _bottomBar.Measure(
            MeasureSpec.MakeMeasureSpec(width, MeasureSpecMode.Exactly),
            MeasureSpec.MakeMeasureSpec(_bottomBar.ItemContainer.MeasuredHeight, MeasureSpecMode.Exactly));

        int barHeight = _bottomBar.ItemContainer.MeasuredHeight;

        _bottomBar.Layout(0, b - barHeight, width, b);

        float density = Android.Content.Res.Resources.System.DisplayMetrics.Density;

        double contentWidthConstraint = width / density;
        double contentHeightConstraint = (height - barHeight) / density;

        if (_currentPage != null)
        {
            var renderer = Platform.GetRenderer(_currentPage);

            renderer.Element.Measure(contentWidthConstraint, contentHeightConstraint);
            renderer.Element.Layout(new Rectangle(0, 0, contentWidthConstraint, contentHeightConstraint));

            renderer.UpdateLayout();
        }
    }

    private void InitializeElement(MainPage element)
    {
        PopulateChildren(element);
    }

    private void PopulateChildren(MainPage element)
    {
        // Unfortunately bottom bar can not be reused so we have to
        // remove it and create the new instance
        _bottomBar?.RemoveFromParent();

        _bottomBar = CreateBottomBar(element.Children);
        AddView(_bottomBar);

        LoadPageContent(0);
    }

    private BottomBar CreateBottomBar(IEnumerable&amp;amp;lt;Page&amp;amp;gt; pageIntents)
    {
        var bar = new BottomBar(Context);

        // TODO: Configure the bottom bar here according to your needs

        bar.SetOnTabClickListener(this);
        bar.UseFixedMode();

        PopulateBottomBarItems(bar, pageIntents);

        bar.ItemContainer.SetBackgroundColor(Color.LightGray);

        return bar;
    }

    private void PopulateBottomBarItems(BottomBar bar, IEnumerable&amp;amp;lt;Page&amp;amp;gt; pages)
    {
        var barItems = pages.Select(x =&amp;amp;gt; new BottomBarTab(Context.Resources.GetDrawable(x.Icon), x.Title));

        bar.SetItems(barItems.ToArray());
    }

    private void LoadPageContent(int position)
    {
        ShowPage(position);
    }

    private void ShowPage(int position)
    {
        if (position != _lastSelectedTabIndex)
        {
            Element.CurrentPage = Element.Children[position];

            if (Element.CurrentPage != null)
            {
                LoadPageContent(Element.CurrentPage);
            }
        }

        _lastSelectedTabIndex = position;
    }

    private void LoadPageContent(Page page)
    {
        UnloadCurrentPage();

        _currentPage = page;

        LoadCurrentPage();

        Element.CurrentPage = _currentPage;
    }

    private void LoadCurrentPage()
    {
        var renderer = Platform.GetRenderer(_currentPage);

        if (renderer == null)
        {
            renderer = Platform.CreateRenderer(_currentPage);
            Platform.SetRenderer(_currentPage, renderer);

            AddView(renderer.ViewGroup);
        }
        else
        {
            // As we show and hide pages manually OnAppearing and OnDisappearing
            // workflow methods won't be called by the framework. Calling them manually...
            var basePage = _currentPage as BaseContentPage;
            basePage?.SendAppearing();
        }

        renderer.ViewGroup.Visibility = ViewStates.Visible;
    }

    private void UnloadCurrentPage()
    {
        if (_currentPage != null)
        {
            var basePage = _currentPage as BaseContentPage;
            basePage?.SendDisappearing();

            var renderer = Platform.GetRenderer(_currentPage);

            if (renderer != null)
            {
                renderer.ViewGroup.Visibility = ViewStates.Invisible;
            }
        }
    }
}

That’s it! We’re getting the custom tabbed page implementation for Android leaving the iOS part unchanged.

What’s next?

This implementation doesn’t take in account the case when you need to display too many pages collapsing them into “more” section as iOS does. To achieve this you will need to add the “more” item manually into the bottom bar and handle selecting of it by rendering the MoreMenu Xamarin view into native ViewGroup and showing it over the content.

Full source code of this example: https://github.com/smetlov/XamarinFormsAndroidBottomMenu

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s