7

Xamarin forms – Android label less bottom tabs with badges

 3 years ago
source link: https://depblog.weblogs.us/2018/12/18/xamarin-forms-android-label-less-bottom-tabs-with-badges/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Xamarin forms – Android label less bottom tabs with badges

Since a while we are able to render a bottom tabbar in Xamarin Forms for Android!
If you want to know the whole setup, take a look at this great blog post by James Montemagno here…

So recently we added this in our production app, because our own implementation was not always working 100% correct depending on the Android version it was being shown.

But we also needed some extra functionality and didn’t want to bring in an external library. So let me show you what we needed and how we added this to Xamarin Forms Android.

First extra needed feature was the ability to show tab badges. In other words a counter that shows, in our case, the amount of unread messages.
End result should be:

Screen-Shot-2018-12-18-at-13.55.01.png

Now the fact that we eventually also need to port this to iOS, means we need some abstracted elements that reside in Xamarin Forms project and not in the os specific projects.
First we need a small class that can contain the badge info that we want to show, it has 2 properties, one for the badge caption and one for the badge color.

public class TabData : INotifyPropertyChanged
    private int _badgeCaption;
    public int BadgeCaption
        get => _badgeCaption;
            _badgeCaption = value;
            OnPropertyChanged();
    private Color _badgeColor;
    public Color BadgeColor
        get => _badgeColor;
            _badgeColor = value;
            OnPropertyChanged();
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
        if (PropertyChanged == null)
            return;
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

So when you want to dynamically change the color or caption, you just change the data in the properties and the code on os level will handle this.
Because we want to interact on os level, we need to create a Custom Renderer. To be sure it can get a hold of the TabData class we also create our own TabbedPage first.

public class BottomTabbedPage : TabbedPage
    public bool Labels { get; set; }
    public Dictionary<int, TabData> Tabs = new Dictionary<int, TabData>();
    public BottomTabbedPage()
        Tabs.Add(0, new TabData() { BadgeColor = Color.RoyalBlue , BadgeCaption = 5 });

I pre-init this page with a badge for the first tab that is blue and has 5 as caption.
With that in place we can start adding our Android Custom renderer.

The actual end goal of the custom renderer is to inject a badge control in each tab where needed. So we have to go through the Tabs dictionary of the BottomTabbedPage to see if we need to add a badge or not. Also we want to hook into the PropertyChanged event of the BadgeData class, so that we can actually change the visuals while the app is running.
This is all handled in the OnElementChanged method of the Custom Renderer.

BottomTabbedPage formsPage = (BottomTabbedPage)Element;
if (e.NewElement != null)
    var relativeLayout = this.GetChildAt(0) as Android.Widget.RelativeLayout;
    if (relativeLayout != null)
        var bottomNavigationView = relativeLayout.GetChildAt(1) as BottomNavigationView;
        bottomNavigationView.SetShiftMode(false, false);
        BottomNavigationMenuView bottomNavigationMenuView = (BottomNavigationMenuView)bottomNavigationView.GetChildAt(0);
        int tabCount = formsPage.Tabs.Count;
        for (int i = 0; i < tabCount; i++)
            var tabData = formsPage.Tabs[0];
            BottomNavigationItemView tabItemView = (BottomNavigationItemView)bottomNavigationMenuView.GetChildAt(i);
            if (tabData.BadgeCaption > 0)
                if (_badgeId == 0)
                    _badgeId = Android.Views.View.GenerateViewId();
                TextView badgeTextView = new BadgeView(Context) { Id = _badgeId, BadgeCaption = tabData.BadgeCaption.ToString(), BadgeColor = tabData.BadgeColor.ToAndroid() };
                tabData.PropertyChanged += (sender, args) =>
                    TabData currentTabData = (TabData)sender;
                    BottomNavigationItemView currentTabItemView = _tabViews[currentTabData];
                    BadgeView currentBadgeTextView = currentTabItemView.FindViewById(_badgeId) as BadgeView;
                    if (currentBadgeTextView != null)
                        currentBadgeTextView.BadgeColor = currentTabData.BadgeColor.ToAndroid();
                        currentBadgeTextView.BadgeCaption = currentTabData.BadgeCaption > 0 ? currentTabData.BadgeCaption.ToString() : string.Empty;
                tabItemView.AddView(badgeTextView);
                _tabViews.Add(tabData, tabItemView);

A bunch of code, but it’s not that difficult to understand… first we adjust the Shift Mode so that our tabs are always shown equally ( reference the blog post of James on top ).
After that we go through the defined TabData elements and get the corresponding BottomNavigationItemView ( the actual item being rendered ).

Note that in my example, I only have a badge on the first tab. When you want badges on each, you need to pre init the TabData list for each tab.

When we want to add a badge, we create a new TextView element and add it to the corresponding BottomNavigationItemView. The TextView itself is just text but with a circular background element ShapeDrawable, that inherits the color you specified in the BadgeData.
All this code can be seen in the BadgeView class.

Also note that we keep track of the PropertyChanged event, so that we alter the color and caption if needed of the presented BadgeView.
And that’s it for our first feature! Bottom tabs with badges for Android.

But we also wanted to have the ability to show only icons on the tabs, so loosing the labels. This will of course only work when you have very descriptive icons.
This should look like so:

Screen-Shot-2018-12-18-at-14.17.24.png

Now normally this should be very easy in Android API 28, because there you can provide a value stating if you want labels or not… but since of this writing Xamarin Forms is not yet 28 compatible ( https://github.com/xamarin/Xamarin.Forms/issues/3083 ) so we need our own implementation.

For this to work, we will strip out the labels that are available in the TabView. But that is not enough, we also need to shift down the icon. Because when you loose the labels, the icon will not be in the center of the TabView. On our BottomTabbedPage class we already added a bool property Labels that we can set in XAML to indicate if we want tabs with or without labels.

In the OnElementChanged we strip out the labels.

if (!formsPage.Labels)
    var childViews = ViewGroup.GetViewsByType(typeof(BottomNavigationItemView));
    foreach (var childView in childViews)
        childView.FindAndRemoveById(Resource.Id.largeLabel);
        childView.FindAndRemoveById(Resource.Id.smallLabel);

And in the OnLayout we shift down the icons ( we only have the tab height at that point ).

protected override void OnLayout(bool changed, int l, int t, int r, int b)
    base.OnLayout(changed, l, t, r, b);
    BottomTabbedPage formsPage = (BottomTabbedPage)Element;
    if (!_bottomBarInit && !formsPage.Labels)
        var childViews = ViewGroup.GetViewsByType(typeof(BottomNavigationItemView));
        int dpAsPixels = 0;
        foreach (var childView in childViews)
            if (dpAsPixels == 0)
                var imageIcon = childView.FindViewById(Resource.Id.icon);
                var parentHeightHalf = ((ViewGroup)childView.Parent).MeasuredHeight / 2;
                var iconHeightHalf = imageIcon.MeasuredHeight / 2;
                var iconTop = imageIcon.Top;
                dpAsPixels = parentHeightHalf - iconHeightHalf - iconTop;
            childView.SetPadding(childView.PaddingLeft, dpAsPixels, childView.PaddingRight, childView.PaddingBottom);
        _bottomBarInit = true;

So we take the actual height of the tab itself, divided by 2 to get the center. Than we grab the height of the icon and also divide this by 2 and also get the current top position of the icon. With those values we now know how much pixels we need to add as top padding to our icon to get it vertically center.

All in all not that much code, but as always with android, you need to know how the actual objects are called and rendered.

As always working copy can be found here…


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK