diff --git a/JournalApp/Data/PreferenceService.cs b/JournalApp/Data/PreferenceService.cs index d23fea3..22cb2b4 100644 --- a/JournalApp/Data/PreferenceService.cs +++ b/JournalApp/Data/PreferenceService.cs @@ -50,6 +50,7 @@ public PreferenceService(ILogger logger, IPreferences prefere { _application = Application.Current; _application.RequestedThemeChanged += Application_RequestedThemeChanged; + } ApplyPlatformTheme(); @@ -211,16 +212,46 @@ public void ApplyPlatformTheme() var surface = IsDarkMode ? GetTheme().PaletteDark.Background : GetTheme().PaletteLight.Background; var background = Color.FromRgb(surface.R, surface.G, surface.B); - // On Android 15 and up the system bars are transparent, so what shows behind them is this page background rather than any status bar color we set. + // The page shows through until the web view paints its first frame, so it carries the surface tone as well. if (_application.Windows.Count > 0 && _application.Windows[0].Page is ContentPage page) page.BackgroundColor = background; - if (OperatingSystem.IsAndroid()) +#if ANDROID + // Go through the MAUI window so this reaches the activity currently hosting it, which Platform.CurrentActivity can lag behind during a relaunch. + var window = (_application.Windows.Count > 0 ? _application.Windows[0].Handler?.PlatformView as Android.App.Activity : null)?.Window; + if (window is null) + return; + + var platformColor = Android.Graphics.Color.Rgb(surface.R, surface.G, surface.B); + + // The web view draws under the transparent bars on Android 15 and up, so icon contrast is the only knob left and it has to follow the in-app theme. + // MAUI guesses it once at startup from the theme's colorPrimary, which is a dark purple in both modes, so it always lands on light icons. + var controller = AndroidX.Core.View.WindowCompat.GetInsetsController(window, window.DecorView); + controller.AppearanceLightStatusBars = !IsDarkMode; + controller.AppearanceLightNavigationBars = !IsDarkMode; + + // Three-button navigation gets a translucent scrim by default, which would grey the surface showing through the bar. + if (OperatingSystem.IsAndroidVersionAtLeast(29)) + { + window.StatusBarContrastEnforced = false; + window.NavigationBarContrastEnforced = false; + } + + // The window itself is what shows through the transparent bars whenever the page is letterboxed, like on a web view too old to report the insets. + window.SetBackgroundDrawable(new Android.Graphics.Drawables.ColorDrawable(platformColor)); + + // Below Android 15 the bars are opaque, so tint them to the surface tone instead. + // Setting an opaque status bar color through the toolkit also makes the decor fit the system windows again, which letterboxes the page inside the bars. + // That is intentional there: those versions ship web views too old to pad for the bars themselves, and the themed opaque bars read the same as drawing under them. + if (!OperatingSystem.IsAndroidVersionAtLeast(35)) { - // Still needed below Android 15, where the status bar has its own color instead of showing the page through. StatusBar.SetColor(background); StatusBar.SetStyle(IsDarkMode ? StatusBarStyle.LightContent : StatusBarStyle.DarkContent); +#pragma warning disable CA1422 // Deprecated in API 35, which the surrounding check excludes. + window.SetNavigationBarColor(platformColor); +#pragma warning restore CA1422 } +#endif } public string GetMoodColor(string emoji) diff --git a/JournalApp/JournalApp.csproj b/JournalApp/JournalApp.csproj index e5fe59f..bc8c75a 100644 --- a/JournalApp/JournalApp.csproj +++ b/JournalApp/JournalApp.csproj @@ -50,7 +50,7 @@ - + @@ -59,7 +59,7 @@ - + diff --git a/JournalApp/MainPage.xaml b/JournalApp/MainPage.xaml index 2562bb7..2b6d45d 100644 --- a/JournalApp/MainPage.xaml +++ b/JournalApp/MainPage.xaml @@ -1,11 +1,11 @@ - + - + diff --git a/JournalApp/MainPage.xaml.cs b/JournalApp/MainPage.xaml.cs index 605f12f..2afb729 100644 --- a/JournalApp/MainPage.xaml.cs +++ b/JournalApp/MainPage.xaml.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Components.WebView; +using Microsoft.AspNetCore.Components.WebView; namespace JournalApp; @@ -7,7 +7,21 @@ public partial class MainPage : ContentPage public MainPage() { InitializeComponent(); + +#if ANDROID + // Only WebView 144 and up forward the system bar insets to CSS, so an older one keeps the page letterboxed inside the bars instead of drawing under them blind. + if (!WebViewReportsSafeArea()) + SafeAreaEdges = new SafeAreaEdges(SafeAreaRegions.Container); +#endif + } + +#if ANDROID + private static bool WebViewReportsSafeArea() + { + var version = Android.Webkit.WebView.CurrentWebViewPackage?.VersionName; + return version is not null && int.TryParse(version.Split('.')[0], out var major) && major >= 144; } +#endif // The native WebView paints white before Blazor's first frame, which flashes hard against a dark splash, so let the themed page background show through instead. private void OnBlazorWebViewInitialized(object sender, BlazorWebViewInitializedEventArgs e) diff --git a/JournalApp/MauiProgram.cs b/JournalApp/MauiProgram.cs index 72ee352..6d32d93 100644 --- a/JournalApp/MauiProgram.cs +++ b/JournalApp/MauiProgram.cs @@ -1,4 +1,5 @@ using CommunityToolkit.Maui; +using Microsoft.Maui.LifecycleEvents; using JournalApp.Data; namespace JournalApp; @@ -24,6 +25,12 @@ public static MauiApp CreateMauiApp() // Enable Blazor WebView. builder.Services.AddMauiBlazorWebView(); +#if ANDROID + // Android relaunches the activity for some configuration changes, like switching the navigation mode, and MAUI resets the system bar appearance each time it connects to one. + builder.ConfigureLifecycleEvents(events => events.AddAndroid(android => android.OnResume(_ => + IPlatformApplication.Current?.Services.GetService()?.ApplyPlatformTheme()))); +#endif + #if DEBUG // Add debugging tools and log output for development. builder.Services.AddBlazorWebViewDeveloperTools(); diff --git a/JournalApp/wwwroot/app.css b/JournalApp/wwwroot/app.css index 47602eb..7f92057 100644 --- a/JournalApp/wwwroot/app.css +++ b/JournalApp/wwwroot/app.css @@ -116,11 +116,13 @@ textarea, font-weight: 400; } +/* The web view draws under the transparent status bar, so the header extends up behind it and carries the surface tone there. */ .page-header { z-index: var(--mud-zindex-appbar); display: flex; flex-direction: column; padding: 0 !important; + padding-top: env(safe-area-inset-top, 0px) !important; margin: 0 !important; top: 0 !important; position: sticky !important; @@ -135,7 +137,7 @@ textarea, } /* On-scroll separation for the sticky header: a soft lift fades in once content passes underneath. */ -/* MD3 proper would retone the container instead, but that desyncs the natively colored status bar, so elevation stands in until edge-to-edge lands. */ +/* MD3 proper would retone the container instead; now that the header owns the status bar region that retone can follow it, so it is a candidate to swap in. */ @supports (animation-timeline: scroll()) { .page-header { animation: header-on-scroll linear both; @@ -157,7 +159,7 @@ textarea, max-width: 960px; margin: 0 auto !important; padding: 8px !important; - padding-bottom: 15vh !important; + padding-bottom: calc(15vh + env(safe-area-inset-bottom, 0px)) !important; animation: fadeInUp var(--ja-motion-spatial); } @@ -463,8 +465,14 @@ label.mud-switch.mud-disabled .mud-switch-span { } /* The keyboard overlays the WebView and shrinks the visual viewport; pinning the container to it glides dialogs out of the keyboard's way. */ +/* The container spans under the system bars as well, so it pads them out; the bottom inset is dropped while the keyboard covers the navigation bar. */ .mud-dialog-container { + box-sizing: border-box; height: var(--visible-height, 100%); + padding-top: env(safe-area-inset-top, 0px); + padding-left: env(safe-area-inset-left, 0px); + padding-right: env(safe-area-inset-right, 0px); + padding-bottom: max(0px, calc(env(safe-area-inset-bottom, 0px) - var(--keyboard-inset, 0px))); transition: height 250ms cubic-bezier(0.2, 0, 0, 1); } @@ -473,9 +481,16 @@ label.mud-switch.mud-disabled .mud-switch-span { max-height: calc(100% - 32px); } -/* Give the document scroll room so content can move up out from under the keyboard. */ +/* Give the document scroll room so content can move up out from under the keyboard, and keep it clear of a side cutout or bar in landscape. */ body { padding-bottom: var(--keyboard-inset, 0px); + padding-left: env(safe-area-inset-left, 0px); + padding-right: env(safe-area-inset-right, 0px); +} + +/* Bottom-anchored chrome floats above the navigation bar instead of under it. */ +.mud-snackbar-location-bottom-center { + bottom: calc(24px + env(safe-area-inset-bottom, 0px)); } /* M3 dialogs sit on surfaceContainerHigh with the 24px container inset, at elevation Level3 rather than MudBlazor's M2 elevation 24. */ @@ -695,30 +710,12 @@ a.list-group-title { display: none; left: 0; padding: 1.25rem; + padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px)); position: fixed; width: 100%; z-index: 1000; } -.status-bar-safe-area { - display: none; -} - -@supports (-webkit-touch-callout: none) { - .status-bar-safe-area { - display: flex; - position: sticky; - top: 0; - height: env(safe-area-inset-top); - width: 100%; - z-index: 1; - } - - .flex-column, .navbar-brand { - padding-left: env(safe-area-inset-left); - } -} - /* Android honors Remove animations under accessibility settings, so drop every entrance, transition and transform the app adds. */ /* The scroll-driven header lift stays: it is tied to scroll position rather than to time, and it is the only cue that content is passing underneath. */ @media (prefers-reduced-motion: reduce) { @@ -743,13 +740,13 @@ a.list-group-title { } @media (prefers-color-scheme: dark) { - .status-bar-safe-area, #splash-screen { + #splash-screen { background-color: #181215; } } @media (prefers-color-scheme: light) { - .status-bar-safe-area, #splash-screen { + #splash-screen { background-color: #FFF8F9; } } diff --git a/JournalApp/wwwroot/index.html b/JournalApp/wwwroot/index.html index 619a34a..113cf70 100644 --- a/JournalApp/wwwroot/index.html +++ b/JournalApp/wwwroot/index.html @@ -14,8 +14,6 @@ -
-

Getting things ready...

diff --git a/global.json b/global.json index f7772fe..3dbe096 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.300", + "version": "10.0.400", "rollForward": "latestPatch", "allowPrerelease": false }