Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Android.Content;
using Android.Content.Res;
using Android.Graphics;
Expand Down Expand Up @@ -30,6 +31,7 @@ internal static class ToolbarExtensions
{
static ColorStateList? _defaultTitleTextColor;
static int? _defaultNavigationIconColor;
static readonly ConditionalWeakTable<AppBarLayout, AppBarBackgroundState> _defaultAppBarBackgrounds = new();

// Track which ToolbarItem should currently be associated with each MenuItem ID to prevent race conditions
// This prevents stale async icon loading callbacks from updating the wrong toolbar items during navigation
Expand Down Expand Up @@ -195,6 +197,99 @@ public static void UpdateBarBackground(this AToolbar nativeToolbar, Toolbar tool
if (Brush.IsNullOrEmpty(barBackground))
nativeToolbar.BackgroundTintMode = null;
}

if (!nativeToolbar.TryUpdateAppBarBackground(toolbar))
{
nativeToolbar.Post(() => nativeToolbar.TryUpdateAppBarBackground(toolbar));
}
}

static bool TryUpdateAppBarBackground(this AToolbar nativeToolbar, Toolbar toolbar)
{
var appBarLayout = nativeToolbar.Parent?.GetParentOfType<AppBarLayout>();
if (appBarLayout is null)
return false;

var backgroundState = GetOrCreateDefaultAppBarBackgroundState(appBarLayout);
var barBackground = toolbar.BarBackground;

if (Brush.IsNullOrEmpty(barBackground))
{
RestoreDefaultAppBarBackground(appBarLayout, backgroundState);
return true;
}

if (barBackground is SolidColorBrush solidColor)
{
if (solidColor.Color is Color tintColor)
{
var platformColor = tintColor.ToPlatform();
appBarLayout.BackgroundTintMode = null;
appBarLayout.BackgroundTintList = null;
appBarLayout.Background = new ColorDrawable(platformColor);
appBarLayout.StatusBarForeground = new ColorDrawable(platformColor);
}
else
{
RestoreDefaultAppBarBackground(appBarLayout, backgroundState);
}

return true;
}

appBarLayout.BackgroundTintMode = null;
appBarLayout.BackgroundTintList = null;
appBarLayout.UpdateBackground(barBackground);
appBarLayout.StatusBarForeground = CloneDrawable(appBarLayout.Background);
return true;
}

static AppBarBackgroundState GetOrCreateDefaultAppBarBackgroundState(AppBarLayout appBarLayout)
{
if (_defaultAppBarBackgrounds.TryGetValue(appBarLayout, out var backgroundState))
return backgroundState;

backgroundState = new AppBarBackgroundState(
CloneDrawable(appBarLayout.Background),
CloneDrawable(appBarLayout.StatusBarForeground),
appBarLayout.BackgroundTintList,
appBarLayout.BackgroundTintMode);

_defaultAppBarBackgrounds.Add(appBarLayout, backgroundState);
return backgroundState;
}

static void RestoreDefaultAppBarBackground(AppBarLayout appBarLayout, AppBarBackgroundState backgroundState)
{
appBarLayout.Background = CloneDrawable(backgroundState.DefaultBackground);
appBarLayout.StatusBarForeground = CloneDrawable(backgroundState.DefaultStatusBarForeground);
appBarLayout.BackgroundTintMode = backgroundState.DefaultBackgroundTintMode;
appBarLayout.BackgroundTintList = backgroundState.DefaultBackgroundTintList;
}

static Drawable? CloneDrawable(Drawable? drawable)
{
if (drawable is null)
return null;

using var constantState = drawable.GetConstantState();
return constantState?.NewDrawable()?.Mutate();
}

sealed class AppBarBackgroundState
{
public AppBarBackgroundState(Drawable? defaultBackground, Drawable? defaultStatusBarForeground, ColorStateList? defaultBackgroundTintList, PorterDuff.Mode? defaultBackgroundTintMode)
{
DefaultBackground = defaultBackground;
DefaultStatusBarForeground = defaultStatusBarForeground;
DefaultBackgroundTintList = defaultBackgroundTintList;
DefaultBackgroundTintMode = defaultBackgroundTintMode;
}

public Drawable? DefaultBackground { get; }
public Drawable? DefaultStatusBarForeground { get; }
public ColorStateList? DefaultBackgroundTintList { get; }
public PorterDuff.Mode? DefaultBackgroundTintMode { get; }
}

public static void UpdateIconColor(this AToolbar nativeToolbar, Toolbar toolbar)
Expand Down
35 changes: 35 additions & 0 deletions src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ protected MaterialToolbar GetPlatformToolbar(IElementHandler handler)
protected string GetToolbarTitle(IElementHandler handler) =>
GetPlatformToolbar(handler).Title;

protected AppBarLayout GetPlatformAppBarLayout(IElementHandler handler)
{
var toolbar = GetPlatformToolbar(handler);

if (toolbar == null)
return null;

return toolbar.Parent.GetParentOfType<AppBarLayout>();
}

protected MaterialToolbar GetPlatformToolbar(IMauiContext mauiContext)
{
var navManager = mauiContext.GetNavigationRootManager();
Expand Down Expand Up @@ -212,6 +222,31 @@ public bool IsNavigationBarVisible(IMauiContext mauiContext)
.LayoutParameters?.Height > 0;
}

protected async Task AssertAppBarTopInsetUsesColor(IElementHandler handler, Color expectedColor)
{
var toolbar = GetPlatformToolbar(handler);
Assert.NotNull(toolbar);

var appBar = GetPlatformAppBarLayout(handler);
Assert.NotNull(appBar);

var platformColor = expectedColor.ToPlatform();

await AssertHelpers.AssertEventually(() =>
{
var backgroundTintList = toolbar.BackgroundTintList;

return backgroundTintList != null &&
backgroundTintList.DefaultColor == platformColor.ToArgb() &&
appBar.PaddingTop > 0 &&
appBar.Width > 0 &&
appBar.Height > appBar.PaddingTop;
}, message: "Toolbar background or AppBarLayout top inset did not update.");

var bitmap = await appBar.ToBitmap(handler.MauiContext);
await bitmap.AssertContainsColor(platformColor, rect => new RectF(0, 0, rect.Width, appBar.PaddingTop));
}

protected static WindowInsetsCompat CreateTopCutoutInsets(int statusBarTopInset, int displayCutoutTopInset)
{
return new WindowInsetsCompat.Builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Handlers;
using Microsoft.Maui.DeviceTests.Stubs;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using Xunit;
Expand Down Expand Up @@ -113,6 +114,30 @@ await AssertEventually(() =>
});
}

[Fact(DisplayName = "NavigationPage bar background colors the edge-to-edge top inset area")]
public async Task BarBackgroundColorColorsEdgeToEdgeTopInsetArea()
{
SetupBuilder();

var expectedColor = Colors.OrangeRed;
var rootPage = new ContentPage
{
Title = "Page Title",
Content = new Label { Text = "NavigationPage content" }
};

var navPage = new NavigationPage(rootPage)
{
BarBackgroundColor = expectedColor
};

await CreateHandlerAndAddToWindow<WindowHandlerStub>(new Window(navPage), async (handler) =>
{
await OnLoadedAsync(rootPage);
await AssertAppBarTopInsetUsesColor(handler, expectedColor);
});
}

[Fact(DisplayName = "NavigationPage push to hidden navigation bar clears app bar inset padding")]
public async Task PushingToPageWithoutNavigationBarClearsAppBarInsetPadding()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.Platform.Compatibility;
using Microsoft.Maui.DeviceTests.Stubs;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Platform;
using Xunit;
Expand Down Expand Up @@ -649,6 +650,32 @@ await CreateHandlerAndAddToWindow<ShellHandler>(shell, async (handler) =>
});
}

[Fact(DisplayName = "Shell background colors the edge-to-edge top inset area")]
public async Task ShellBackgroundColorsEdgeToEdgeTopInsetArea()
{
SetupBuilder();

var expectedColor = Colors.OrangeRed;
var page = new ContentPage
{
Title = "Page Title",
Content = new Label { Text = "Shell content" }
};

Shell.SetBackgroundColor(page, expectedColor);

var shell = new Shell
{
CurrentItem = page
};

await CreateHandlerAndAddToWindow<WindowHandlerStub>(new Controls.Window(shell), async (handler) =>
{
await OnLoadedAsync(page);
await AssertAppBarTopInsetUsesColor(handler, expectedColor);
});
}

protected AView GetFlyoutPlatformView(ShellRenderer shellRenderer)
{
var drawerLayout = GetDrawerLayout(shellRenderer);
Expand Down
Loading