maui icon indicating copy to clipboard operation
maui copied to clipboard

Shell TabBar - OnClicked event or other way to detect click on currently active tab

Open ToolmakerSteve opened this issue 1 year ago • 3 comments

Description

Recently there have been two StackOverflow questions by app authors that would like to detect user click on the currently active tabbar tab, and use that to clear navigation stack (display the root page for that tab).

The existing tab changed event only fires when a different tab is clicked on. Clicking on active tab is ignored, because that is not a "change".

Public API Changes

One way to implement this proposal would be to NOT IGNORE clicks on active tabs. Always call the "changed" event, even though it isn't actually a change.

I don't think this would break existing apps - it would simply re-display what is already showing.

Intended Use-Case

Use cases are shown in two StackOverflow threads:

https://stackoverflow.com/q/76046491/199364

https://stackoverflow.com/q/76343252/199364

What both these authors need, is a place they could put the following code:

Navigation.PopToRootAsync();

And have that code run, if user clicks on currently active tab.

ToolmakerSteve avatar May 26 '23 19:05 ToolmakerSteve

Indeed, this would be extremely helpful in situations where users went far from root so going back with each page is really bothersome. Clicking on the same tab would bring them back.

As for the API changes I think it would be safer to have separate event not the "changed" one.

ziomek64 avatar May 27 '23 03:05 ziomek64

Concur... we'd like to see this as well.

davecarlisle avatar Nov 16 '23 18:11 davecarlisle

Has anyone come up with a solution for this?

AlanHolguin avatar Jan 12 '24 20:01 AlanHolguin

I found a way to achieve this, it's not a comfortable one but it works, basically I'm using ShellRenderer to render my own TabBar. I'm currently building only for Android so I will show only Android but I guess the principle should be similar for other platforms.

  1. In Platforms -> Android -> Resources add MyShellRenderer.cs
using Android.Content.Res;
using Google.Android.Material.BottomNavigation;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Microsoft.Maui.Controls.Handlers.Compatibility;
using Microsoft.Maui.Controls.Platform.Compatibility;

namespace YOUR_PROJECT_NAME.Platforms.Android
{
    public class MyShellRenderer : ShellRenderer
    {
        public MyShellRenderer(Context context) : base(context)
        {
        }
        protected override IShellBottomNavViewAppearanceTracker CreateBottomNavViewAppearanceTracker(ShellItem shellItem)
        {
            return new MyShellBottomNavViewAppearanceTracker();
        }
    }
    public class MyShellBottomNavViewAppearanceTracker : IShellBottomNavViewAppearanceTracker
    {
        public void Dispose()
        {
        }

        public void ResetAppearance(BottomNavigationView bottomView)
        {
        }

        public void SetAppearance(BottomNavigationView bottomView, IShellAppearanceElement appearance)
        {
            //example of how to change how TabBar is looking using native properties
            bottomView.LabelVisibilityMode = LabelVisibilityMode.LabelVisibilityUnlabeled;
            bottomView.BackgroundTintList = ColorStateList.ValueOf(Colors.Gray.ToAndroid());
            bottomView.ItemActiveIndicatorColor = ColorStateList.ValueOf(Colors.White.ToAndroid());
            bottomView.ItemIconTintList = ColorStateList.ValueOf(Colors.White.ToAndroid());
            bottomView.SetPadding(100, 0, 100, 0);
            
            //fired when the same TabBar page is selected again
            bottomView.ItemReselected += ItemReselected ;
        }

        private void ItemReselected (object sender, EventArgs e)
        {
            //do something
        }
    }
}
  1. In MauiProgram.cs add at the end of builder extensions
 .ConfigureMauiHandlers(handlers =>
                {
#if ANDROID
                    handlers.AddHandler(typeof(Shell), typeof(Platforms.Android.MyShellRenderer));
#endif
                });

I hope this helps 👍

OvrBtn avatar Jan 24 '24 00:01 OvrBtn

I want to add that this behaves differently (MAUI 8.0.7 but I think it was the case on MAUI 7 too) on iOS and Android.

You can easily test that by overriding OnNavigating from the Shell class:

protected override void OnNavigating(ShellNavigatingEventArgs args)
{
    base.OnNavigating(args);
    Console.WriteLine($"{args.Source}: {args.Current} -> {args.Target}");
}

On Android ShellSectionChanged will be triggered only when changing tabs but on iOS you will notice OnNavigating being called even when re-selecting the current/active tab. One of the desired behaviours is that re-selecting the current tab will perform PopToRoot on the tab's navigation stack (which works on iOS).

As a work-around, I am going to try do subclass the ShellRenderer as shown by @OvrBtn and use the ItemReselected listener to trigger OnNavigating again.

durandt avatar Mar 21 '24 07:03 durandt

This is the work-around I ended up using:

  • Override your Shell's OnNavigating to trigger PopToRootAsync on Android, when navigating to the same tab which NavigationStack has more than one element and while we are not already Popping (added extra safeguards in case the event should be raised multiple times somehow)
  • Subclass the ShellRenderer for Android and register the BottomView's ItemReselected event. Register it only once, otherwise you will get multiple events (Note: Using the SetAppearance method seems a bit hacky but I did not find a cleaner (and simple) way to access the BottomView)
  • Register the newly created ShellRenderer as a Handler for the Shell component

ShellRenderer

using Android.Content;
using Google.Android.Material.BottomNavigation;
using Microsoft.Maui.Controls.Handlers.Compatibility;
using Microsoft.Maui.Controls.Platform.Compatibility;

namespace Your.Namespace;

/// <summary>
/// Shell renderer to fix tab reselection event issue on Android
/// https://github.com/dotnet/maui/issues/15301
/// </summary>
public class AndroidShellRenderer : ShellRenderer
{
    public AndroidShellRenderer(Context context) : base(context)
    {
    }

    protected override IShellBottomNavViewAppearanceTracker CreateBottomNavViewAppearanceTracker(ShellItem shellItem)
    {
        return new CustomShellBottomNavViewAppearanceTracker(this, shellItem);
    }
}

public class CustomShellBottomNavViewAppearanceTracker : ShellBottomNavViewAppearanceTracker
{
    private readonly IShellContext _shellRenderer;
    private readonly ShellItem _shellItem;
    private bool _subscribedItemReselected;

    public CustomShellBottomNavViewAppearanceTracker(IShellContext shellRenderer, ShellItem shellItem)
        : base(shellRenderer, shellItem)
    {
        _shellRenderer = shellRenderer;
        _shellItem = shellItem;
    }

    public override void SetAppearance(BottomNavigationView bottomView, IShellAppearanceElement appearance)
    {
        if (_subscribedItemReselected) return;
        bottomView.ItemReselected += ItemReselected;
        _subscribedItemReselected = true;
    }

    private void ItemReselected(object? sender, EventArgs e)
    {
        (_shellItem as IShellItemController).ProposeSection(_shellItem.CurrentItem);
    }
}

Shell (for example AppShell.xaml.cs)

public partial class AppShell : Shell
{
    public AppShell()
    {
        InitializeComponent();
    }

    private bool _isPopToRootInProgress;
    protected override void OnNavigating(ShellNavigatingEventArgs args)
    {
        base.OnNavigating(args);
        if (DeviceInfo.Platform == DevicePlatform.Android
            && args.Source == ShellNavigationSource.ShellSectionChanged
            && args is { Current: not null, Target: not null }
            && args.Current.Location == args.Target.Location
            && !_isPopToRootInProgress
            && CurrentPage.Navigation.NavigationStack.Count > 1)
        {
            _isPopToRootInProgress = true;
            CurrentPage.Navigation.PopToRootAsync().ContinueWith(_ =>
            {
                _isPopToRootInProgress = false;
            });
        }
    }
}

Register handler

    builder.ConfigureMauiHandlers(handlers =>
    {
#if ANDROID
        handlers.AddHandler(typeof(Shell), typeof(Your.Namespace.AndroidShellRenderer))
#endif
    });

Reading around the MAUI code you could also probably use the Android ShellItemRenderer.OnTabReselected.

durandt avatar Mar 21 '24 10:03 durandt