diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs index 25ecac653611..d969e5691f27 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs @@ -12,6 +12,7 @@ using Google.Android.Material.BottomSheet; using Google.Android.Material.Navigation; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Maui.Graphics; using AColor = Android.Graphics.Color; using AView = Android.Views.View; @@ -38,7 +39,7 @@ void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) { _shellAppearance = appearance; - if (appearance != null) + if (appearance is not null) SetAppearance(appearance); else ResetAppearance(); @@ -59,6 +60,9 @@ void IAppearanceObserver.OnAppearanceChanged(ShellAppearance appearance) bool _appearanceSet; public IShellItemController ShellItemController => ShellItem; IMauiContext MauiContext => ShellContext.Shell.Handler.MauiContext; + IMenuItem _updateMenuItemTitle; + IMenuItem _updateMenuItemIcon; + ShellSection _updateMenuItemSource; public ShellItemRenderer(IShellContext shellContext) : base(shellContext) { @@ -73,10 +77,10 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, _navigationArea = PlatformInterop.CreateNavigationBarArea(context, _outerLayout); _bottomView = PlatformInterop.CreateNavigationBar(context, Resource.Attribute.bottomNavigationViewStyle, _outerLayout, this); - if (ShellItem == null) + if (ShellItem is null) throw new InvalidOperationException("Active Shell Item not set. Have you added any Shell Items to your Shell?"); - if (ShellItem.CurrentItem == null) + if (ShellItem.CurrentItem is null) throw new InvalidOperationException($"Content not found for active {ShellItem}. Title: {ShellItem.Title}. Route: {ShellItem.Route}."); HookEvents(ShellItem); @@ -92,12 +96,12 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, void Destroy() { - if (ShellItem != null) + if (ShellItem is not null) UnhookEvents(ShellItem); ((IShellController)ShellContext?.Shell)?.RemoveAppearanceObserver(this); - if (_bottomSheetDialog != null) + if (_bottomSheetDialog is not null) { _bottomSheetDialog.DismissEvent -= OnMoreSheetDismissed; _bottomSheetDialog?.Dispose(); @@ -108,7 +112,7 @@ void Destroy() _appearanceTracker?.Dispose(); _outerLayout?.Dispose(); - if (_bottomView != null) + if (_bottomView is not null) { _bottomView?.SetOnItemSelectedListener(null); _bottomView?.Background?.Dispose(); @@ -143,9 +147,9 @@ public override void OnDestroy() protected virtual void SetAppearance(ShellAppearance appearance) { - if (_bottomView == null || + if (_bottomView is null || _bottomView.Visibility == ViewStates.Gone || - DisplayedPage == null) + DisplayedPage is null) { return; } @@ -229,7 +233,7 @@ void clickCallback(object s, EventArgs e) (result) => { image.SetImageDrawable(result?.Value); - if (result?.Value != null) + if (result?.Value is not null) { var color = Colors.Black.MultiplyAlpha(0.6f).ToPlatform(); result.Value.SetTint(color); @@ -287,13 +291,13 @@ protected override void OnDisplayedPageChanged(Page newPage, Page oldPage) { base.OnDisplayedPageChanged(newPage, oldPage); - if (oldPage != null) + if (oldPage is not null) oldPage.PropertyChanged -= OnDisplayedElementPropertyChanged; - if (newPage != null) + if (newPage is not null) newPage.PropertyChanged += OnDisplayedElementPropertyChanged; - if (newPage != null && !_menuSetup) + if (newPage is not null && !_menuSetup) { SetupMenu(); } @@ -358,7 +362,7 @@ protected virtual void OnMoreSheetDismissed(object sender, EventArgs e) { OnShellSectionChanged(); - if (_bottomSheetDialog != null) + if (_bottomSheetDialog is not null) { _bottomSheetDialog.DismissEvent -= OnMoreSheetDismissed; _bottomSheetDialog.Dispose(); @@ -377,24 +381,53 @@ protected override void OnShellSectionPropertyChanged(object sender, PropertyCha { base.OnShellSectionPropertyChanged(sender, e); - if (e.PropertyName == BaseShellItem.IsEnabledProperty.PropertyName) + if (e.IsOneOf(BaseShellItem.TitleProperty, BaseShellItem.IconProperty, BaseShellItem.IsEnabledProperty)) { - var content = (ShellSection)sender; - var index = ((IShellItemController)ShellItem).GetItems().IndexOf(content); + var shellSection = (ShellSection)sender; + var index = ((IShellItemController)ShellItem).GetItems().IndexOf(shellSection); var itemCount = ((IShellItemController)ShellItem).GetItems().Count; var maxItems = _bottomView.MaxItemCount; + IMenuItem menuItem = null; - if (itemCount > maxItems && index > maxItems - 2) - return; + if (!(itemCount > maxItems && index > maxItems - 2)) + { + menuItem = _bottomView.Menu.FindItem(index); + } - var menuItem = _bottomView.Menu.FindItem(index); - UpdateShellSectionEnabled(content, menuItem); - } - else if (e.PropertyName == BaseShellItem.TitleProperty.PropertyName || - e.PropertyName == BaseShellItem.IconProperty.PropertyName) - { - SetupMenu(); + if (menuItem is not null) + { + if (e.Is(BaseShellItem.IsEnabledProperty)) + { + UpdateShellSectionEnabled(shellSection, menuItem); + } + else if (e.Is(BaseShellItem.IconProperty)) + { + _updateMenuItemIcon = menuItem; + } + else if (e.Is(BaseShellItem.TitleProperty)) + { + _updateMenuItemTitle = menuItem; + } + } + + // This is primarily used so that `SetupMenu` is still called when the + // title and icon property change calls happen. We don't want to break users + // that are dependent on that behavior + if (e.IsOneOf(BaseShellItem.IconProperty, BaseShellItem.TitleProperty)) + { + try + { + _updateMenuItemSource = shellSection; + SetupMenu(); + } + finally + { + _updateMenuItemIcon = null; + _updateMenuItemTitle = null; + _updateMenuItemSource = null; + } + } } } @@ -406,7 +439,35 @@ protected virtual void OnTabReselected(ShellSection shellSection) protected virtual void SetupMenu(IMenu menu, int maxBottomItems, ShellItem shellItem) { - if (DisplayedPage == null) + if ((_updateMenuItemIcon is not null || _updateMenuItemTitle is not null) && + _menuSetup && + _updateMenuItemSource is not null && + _bottomView.IsAlive() && + _bottomView.IsAttachedToWindow) + { + if (_updateMenuItemIcon is not null) + { + var menuItem = _updateMenuItemIcon; + var shellSection = _updateMenuItemSource; + _updateMenuItemSource = null; + _updateMenuItemIcon = null; + + UpdateShellSectionIcon(shellSection, menuItem); + return; + } + else if (_updateMenuItemTitle is not null) + { + var menuItem = _updateMenuItemTitle; + var shellSection = _updateMenuItemSource; + _updateMenuItemSource = null; + _updateMenuItemIcon = null; + + UpdateShellSectionTitle(shellSection, menuItem); + return; + } + } + + if (DisplayedPage is null) return; if (ShellItemController.ShowTabs) @@ -427,6 +488,18 @@ protected virtual void SetupMenu(IMenu menu, int maxBottomItems, ShellItem shell UpdateTabBarVisibility(); } + protected virtual void UpdateShellSectionIcon(ShellSection shellSection, IMenuItem menuItem) + { + BottomNavigationViewUtils.SetMenuItemIcon(menuItem, shellSection.Icon, MauiContext) + .FireAndForget(e => MauiContext?.CreateLogger()? + .LogWarning(e, "Failed to Update Shell Section Icon")); + } + + protected virtual void UpdateShellSectionTitle(ShellSection shellSection, IMenuItem menuItem) + { + BottomNavigationViewUtils.SetMenuItemTitle(menuItem, shellSection.Title); + } + protected virtual void UpdateShellSectionEnabled(ShellSection shellSection, IMenuItem menuItem) { bool tabEnabled = shellSection.IsEnabled; @@ -453,12 +526,12 @@ void SetupMenu() protected virtual void UpdateTabBarVisibility() { - if (DisplayedPage == null) + if (DisplayedPage is null) return; _bottomView.Visibility = ShellItemController.ShowTabs ? ViewStates.Visible : ViewStates.Gone; - if (_shellAppearance != null && !_appearanceSet) + if (_shellAppearance is not null && !_appearanceSet) { SetAppearance(_shellAppearance); } diff --git a/src/Controls/src/Core/Platform/Android/BottomNavigationViewUtils.cs b/src/Controls/src/Core/Platform/Android/BottomNavigationViewUtils.cs index baf0d9b6418f..ad2b610edc2e 100644 --- a/src/Controls/src/Core/Platform/Android/BottomNavigationViewUtils.cs +++ b/src/Controls/src/Core/Platform/Android/BottomNavigationViewUtils.cs @@ -1,6 +1,7 @@ #nullable disable using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Android.Content; using Android.Graphics.Drawables; @@ -39,6 +40,29 @@ internal static void UpdateEnabled(bool tabEnabled, IMenuItem menuItem) menuItem.SetEnabled(tabEnabled); } + internal static Task SetupMenuItem( + (string title, ImageSource icon, bool tabEnabled) item, + IMenu menu, + int index, + int currentIndex, + BottomNavigationView bottomView, + IMauiContext mauiContext, + out IMenuItem menuItem) + { + Task returnValue; + using var title = new Java.Lang.String(item.title); + menuItem = menu.Add(0, index, 0, title); + returnValue = SetMenuItemIcon(menuItem, item.icon, mauiContext); + UpdateEnabled(item.tabEnabled, menuItem); + if (index == currentIndex) + { + menuItem.SetChecked(true); + bottomView.SelectedItemId = index; + } + + return returnValue; + } + internal static async void SetupMenu( IMenu menu, int maxBottomItems, @@ -48,29 +72,33 @@ internal static async void SetupMenu( IMauiContext mauiContext) { Context context = mauiContext.Context; - menu.Clear(); + + while (items.Count < menu.Size()) + { + menu.RemoveItem(menu.GetItem(menu.Size() - 1).ItemId); + } + int numberOfMenuItems = items.Count; bool showMore = numberOfMenuItems > maxBottomItems; int end = showMore ? maxBottomItems - 1 : numberOfMenuItems; - List menuItems = new List(); List loadTasks = new List(); for (int i = 0; i < end; i++) { var item = items[i]; - using (var title = new Java.Lang.String(item.title)) + + IMenuItem menuItem; + if (i >= menu.Size()) + loadTasks.Add(SetupMenuItem(item, menu, i, currentIndex, bottomView, mauiContext, out menuItem)); + else { - var menuItem = menu.Add(0, i, 0, title); - menuItems.Add(menuItem); + menuItem = menu.GetItem(i); + SetMenuItemTitle(menuItem, item.title); loadTasks.Add(SetMenuItemIcon(menuItem, item.icon, mauiContext)); - UpdateEnabled(item.tabEnabled, menuItem); - if (i == currentIndex) - { - menuItem.SetChecked(true); - bottomView.SelectedItemId = i; - } } + + menuItems.Add(menuItem); } if (showMore) @@ -88,17 +116,20 @@ internal static async void SetupMenu( if (loadTasks.Count > 0) await Task.WhenAll(loadTasks); + } - foreach (var menuItem in menuItems) - menuItem.Dispose(); + internal static void SetMenuItemTitle(IMenuItem menuItem, string title) + { + using var jTitle = new Java.Lang.String(title); + menuItem.SetTitle(jTitle); } - static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, IMauiContext context) + internal static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, IMauiContext context) { if (!menuItem.IsAlive()) return; - if (source == null) + if (source is null) return; var services = context.Services; @@ -109,8 +140,10 @@ static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, IMauiC source, context.Context); - if (menuItem.IsAlive() && result is not null) - menuItem.SetIcon(result.Value); + if (menuItem.IsAlive()) + { + menuItem.SetIcon(result?.Value); + } } public static BottomSheetDialog CreateMoreBottomSheet( @@ -212,7 +245,7 @@ public static void SetShiftMode(this BottomNavigationView bottomNavigationView, try { var menuView = bottomNavigationView.GetChildAt(0) as BottomNavigationMenuView; - if (menuView == null) + if (menuView is null) { System.Diagnostics.Debug.WriteLine("Unable to find BottomNavigationMenuView"); return; diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index db8c276793a2..d651671f62ec 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -48,6 +48,8 @@ override Microsoft.Maui.Controls.SearchBar.IsEnabledCore.get -> bool *REMOVED*~static Microsoft.Maui.Controls.MessagingCenter.Subscribe(object subscriber, string message, System.Action callback, TSender source = null) -> void *REMOVED*~static Microsoft.Maui.Controls.MessagingCenter.Unsubscribe(object subscriber, string message) -> void *REMOVED*~static Microsoft.Maui.Controls.MessagingCenter.Unsubscribe(object subscriber, string message) -> void +~virtual Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.UpdateShellSectionIcon(Microsoft.Maui.Controls.ShellSection shellSection, Android.Views.IMenuItem menuItem) -> void +~virtual Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.UpdateShellSectionTitle(Microsoft.Maui.Controls.ShellSection shellSection, Android.Views.IMenuItem menuItem) -> void *REMOVED*~Microsoft.Maui.Controls.OpenGLView.On() -> Microsoft.Maui.Controls.IPlatformElementConfiguration *REMOVED*~Microsoft.Maui.Controls.OpenGLView.OnDisplay.get -> System.Action *REMOVED*~Microsoft.Maui.Controls.OpenGLView.OnDisplay.set -> void @@ -71,4 +73,4 @@ override Microsoft.Maui.Controls.SearchBar.IsEnabledCore.get -> bool *REMOVED*Microsoft.Maui.Controls.Application.SavePropertiesAsync() -> System.Threading.Tasks.Task! *REMOVED*Microsoft.Maui.Controls.Application.Properties.get -> System.Collections.Generic.IDictionary! Microsoft.Maui.Controls.IValueConverter.Convert(object? value, System.Type! targetType, object? parameter, System.Globalization.CultureInfo! culture) -> object? -Microsoft.Maui.Controls.IValueConverter.ConvertBack(object? value, System.Type! targetType, object? parameter, System.Globalization.CultureInfo! culture) -> object? \ No newline at end of file +Microsoft.Maui.Controls.IValueConverter.ConvertBack(object? value, System.Type! targetType, object? parameter, System.Globalization.CultureInfo! culture) -> object? diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs index 4bc3a557b9b6..1a4980544c37 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs @@ -10,6 +10,7 @@ using AndroidX.DrawerLayout.Widget; using AndroidX.ViewPager2.Widget; using Google.Android.Material.AppBar; +using Google.Android.Material.BottomNavigation; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Handlers.Compatibility; using Microsoft.Maui.Controls.Platform; @@ -222,6 +223,150 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } + [Fact] + public async Task SwappingOutAndroidContextDoesntCrash() + { + SetupBuilder(); + + var shell = await CreateShellAsync(shell => + { + shell.Items.Add(new FlyoutItem() { Route = "FlyoutItem1", Items = { new ContentPage() }, Title = "Flyout Item" }); + shell.Items.Add(new FlyoutItem() { Route = "FlyoutItem2", Items = { new ContentPage() }, Title = "Flyout Item" }); + }); + + var window = new Controls.Window(shell); + var mauiContextStub1 = new ContextStub(MauiContext.GetApplicationServices()); + var activity = mauiContextStub1.GetActivity(); + mauiContextStub1.Context = new ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); + + await CreateHandlerAndAddToWindow(window, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + await OnNavigatedToAsync(shell.CurrentPage); + await Task.Delay(100); + await shell.GoToAsync("//FlyoutItem2"); + }, mauiContextStub1); + + var mauiContextStub2 = new ContextStub(MauiContext.GetApplicationServices()); + mauiContextStub2.Context = new ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); + + await CreateHandlerAndAddToWindow(window, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + await OnNavigatedToAsync(shell.CurrentPage); + await Task.Delay(100); + await shell.GoToAsync("//FlyoutItem1"); + await shell.GoToAsync("//FlyoutItem2"); + }, mauiContextStub2); + } + + [Fact] + public async Task ChangingBottomTabAttributesDoesntRecreateBottomTabs() + { + SetupBuilder(); + + var shell = await CreateShellAsync(shell => + { + shell.Items.Add(new Tab() { Items = { new ContentPage() }, Title = "Tab 1", Icon = "red.png" }); + shell.Items.Add(new Tab() { Items = { new ContentPage() }, Title = "Tab 2", Icon = "red.png" }); + }); + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + var menu = GetDrawerLayout(handler).GetFirstChildOfType().Menu; + var menuItem1 = menu.GetItem(0); + var menuItem2 = menu.GetItem(1); + var icon1 = menuItem1.Icon; + var icon2 = menuItem2.Icon; + var title1 = menuItem1.TitleFormatted; + var title2 = menuItem2.TitleFormatted; + + shell.CurrentItem.Items[0].Title = "new Title 1"; + shell.CurrentItem.Items[0].Icon = "blue.png"; + + shell.CurrentItem.Items[1].Title = "new Title 2"; + shell.CurrentItem.Items[1].Icon = "blue.png"; + + // let the icon and title propagate + await AssertionExtensions.Wait(() => menuItem1.Icon != icon1); + + menu = GetDrawerLayout(handler).GetFirstChildOfType().Menu; + Assert.Equal(menuItem1, menu.GetItem(0)); + Assert.Equal(menuItem2, menu.GetItem(1)); + + menuItem1.Icon.AssertColorAtCenter(Android.Graphics.Color.Blue); + menuItem2.Icon.AssertColorAtCenter(Android.Graphics.Color.Blue); + + Assert.NotEqual(icon1, menuItem1.Icon); + Assert.NotEqual(icon2, menuItem2.Icon); + Assert.NotEqual(title1, menuItem1.TitleFormatted); + Assert.NotEqual(title2, menuItem2.TitleFormatted); + }); + } + + [Fact] + public async Task RemovingBottomTabDoesntRecreateMenu() + { + SetupBuilder(); + + var shell = await CreateShellAsync(shell => + { + shell.Items.Add(new Tab() { Items = { new ContentPage() }, Title = "Tab 1", Icon = "red.png" }); + shell.Items.Add(new Tab() { Items = { new ContentPage() }, Title = "Tab 2", Icon = "red.png" }); + shell.Items.Add(new Tab() { Items = { new ContentPage() }, Title = "Tab 3", Icon = "red.png" }); + }); + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + var bottomView = GetDrawerLayout(handler).GetFirstChildOfType(); + var menu = bottomView.Menu; + var menuItem1 = menu.GetItem(0); + var menuItem2 = menu.GetItem(1); + + shell.CurrentItem.Items.RemoveAt(2); + + // let the change propagate + await AssertionExtensions.Wait(() => bottomView.Menu.Size() == 2); + + menu = bottomView.Menu; + Assert.Equal(menuItem1, menu.GetItem(0)); + Assert.Equal(menuItem2, menu.GetItem(1)); + }); + } + + [Fact] + public async Task AddingBottomTabDoesntRecreateMenu() + { + SetupBuilder(); + + var shell = await CreateShellAsync(shell => + { + shell.Items.Add(new Tab() { Items = { new ContentPage() }, Title = "Tab 1", Icon = "red.png" }); + shell.Items.Add(new Tab() { Items = { new ContentPage() }, Title = "Tab 3", Icon = "red.png" }); + }); + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + var bottomView = GetDrawerLayout(handler).GetFirstChildOfType(); + var menu = bottomView.Menu; + var menuItem1 = menu.GetItem(0); + var menuItem2 = menu.GetItem(1); + var menuItem2Icon = menuItem2.Icon; + + shell.CurrentItem.Items.Insert(1, new Tab() { Items = { new ContentPage() }, Title = "Tab 2", Icon = "green.png" }); + + // let the change propagate + await AssertionExtensions.Wait(() => bottomView.Menu.GetItem(1).Icon != menuItem2Icon); + + menu = bottomView.Menu; + Assert.Equal(menuItem1, menu.GetItem(0)); + Assert.Equal(menuItem2, menu.GetItem(1)); + + menu.GetItem(1).Icon.AssertColorAtCenter(Android.Graphics.Color.Green); + menu.GetItem(2).Icon.AssertColorAtCenter(Android.Graphics.Color.Red); + }); + } + protected AView GetFlyoutPlatformView(ShellRenderer shellRenderer) { var drawerLayout = GetDrawerLayout(shellRenderer); diff --git a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs index fae6475a8df1..fbe4d0677c7b 100644 --- a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.Android.cs @@ -1,13 +1,22 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AndroidX.CoordinatorLayout.Widget; using AndroidX.ViewPager2.Widget; +using Google.Android.Material.BottomNavigation; using Microsoft.Maui.Controls; using Microsoft.Maui.DeviceTests.Stubs; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; using Xunit; namespace Microsoft.Maui.DeviceTests { - public partial class TabbedPageTests + [Category(TestCategory.TabbedPage)] + public partial class TabbedPageTests : ControlsHandlerTestBase { [Fact(DisplayName = "Using SelectedTab Color doesnt crash")] public async Task SelectedTabColorNoDoesntCrash() @@ -24,5 +33,58 @@ await CreateHandlerAndAddToWindow(new Window(tabbedPage), (ha return Task.CompletedTask; }); } + + [Fact] + public async Task ChangingBottomTabAttributesDoesntRecreateBottomTabs() + { + SetupBuilder(); + + var tabbedPage = CreateBasicTabbedPage(true, pages: new[] + { + new ContentPage() { Title = "Tab 1", IconImageSource = "red.png" }, + new ContentPage() { Title = "Tab 2", IconImageSource = "red.png" } + }); + + await CreateHandlerAndAddToWindow(tabbedPage, async (handler) => + { + var menu = GetBottomNavigationView(handler).Menu; + var menuItem1 = menu.GetItem(0); + var menuItem2 = menu.GetItem(1); + var icon1 = menuItem1.Icon; + var icon2 = menuItem2.Icon; + var title1 = menuItem1.TitleFormatted; + var title2 = menuItem2.TitleFormatted; + + tabbedPage.Children[0].Title = "new Title 1"; + tabbedPage.Children[0].IconImageSource = "blue.png"; + + tabbedPage.Children[1].Title = "new Title 2"; + tabbedPage.Children[1].IconImageSource = "blue.png"; + + // let the icon and title propagate + await AssertionExtensions.Wait(() => menuItem1.Icon != icon1); + + menu = GetBottomNavigationView(handler).Menu; + Assert.Equal(menuItem1, menu.GetItem(0)); + Assert.Equal(menuItem2, menu.GetItem(1)); + + menuItem1.Icon.AssertColorAtCenter(Android.Graphics.Color.Blue); + menuItem2.Icon.AssertColorAtCenter(Android.Graphics.Color.Blue); + + Assert.NotEqual(icon1, menuItem1.Icon); + Assert.NotEqual(icon2, menuItem2.Icon); + Assert.NotEqual(title1, menuItem1.TitleFormatted); + Assert.NotEqual(title2, menuItem2.TitleFormatted); + }); + } + + + BottomNavigationView GetBottomNavigationView(TabbedViewHandler tabViewHandler) + { + var layout = (tabViewHandler.PlatformView as Android.Views.IViewParent).FindParent((view) => view is CoordinatorLayout) + as CoordinatorLayout; + + return layout.GetFirstChildOfType(); + } } -} \ No newline at end of file +}