Skip to content

[BUG] StatusBarBehavior causes Android modal (DialogFragment) content to render behind the status bar - #3354

Open
TheCodeTraveler wants to merge 18 commits into
mainfrom
bugfix/modal-statusbar-behind-system-bars
Open

TheCodeTraveler wants to merge 18 commits into
mainfrom
bugfix/modal-statusbar-behind-system-bars

Conversation

@TheCodeTraveler

@TheCodeTraveler TheCodeTraveler commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Description of Change

This PR updates Android modal status-bar handling to align with host-window edge-to-edge behavior and refreshes the demonstration sample. Fixes the modal (DialogFragment) path of #2351.

PR #3189 fixed the host-window path (StatusBar.android.cs), however a ContentPage shown modally (Navigation.PushModalAsync) is hosted in a separate DialogFragment window configured by DialogFragmentService.android.cs. This PR fixes that specific scenario.

Linked Issues

PR Checklist

  • Has a linked Issue, and the Issue has been approved(bug) or Championed (feature/proposal)
  • Has tests (if omitted, state reason in description)
  • Has samples (if omitted, state reason in description)
  • Rebased on top of main at time of PR
  • Changes adhere to coding standard
  • Documentation created or updated: https://github.com/MicrosoftDocs/CommunityToolkit/pulls

Additional information

I verified sample app on-device on Android API 26, 33, and 36.1 emulators

The StatusBarBehavior sample is also polished and turned into a regression test for #2351:

  • BrokenTitleViewShell / OverflowingTitleViewPage are renamed to EdgeToEdgeTitleViewShell / EdgeToEdgeTitleViewPage
  • The demo now shows content drawn edge-to-edge under a custom Shell.TitleView, with a toggle to switch the status bar color (blue ↔ red) to confirm the styling, and a Back button to return to the demo.
Sample App Page Android 28 Android 33 Android 36.1
StatusBar Modal Page Demo
StatusBar Shell Demo

Copilot AI balanced review requested due to automatic review settings October 15, 2026 09:34

This comment was marked as outdated.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

This comment was marked as outdated.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

This comment was marked as outdated.

@TheCodeTraveler

TheCodeTraveler commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Hey @Pastajello! Could you review this PR and test it out on an Android Emulator or two when you get a chance? It builds on top of the work you did in #3189, fixing the modal (DialogFragment) path of #2351.

I verified this PR on-device on Android API 26, 33, and 36.1 emulators, and it all looks good!

This comment was marked as outdated.

TheCodeTraveler and others added 6 commits October 16, 2026 01:14
The Sample now has one demo for each fixed path, both opened from the
StatusBarBehavior page (Android only). Each demo says on screen what to
check and what the correct result looks like.

- MauiProgram enables SetShouldUseStatusBarBehaviorOnAndroidModalPage in
  the Debug and Release setup. It is off by default, so the modal demo
  never reached DialogFragmentService before.
- EdgeToEdgeModalPage is the DialogFragment demo (#3353). It has amber
  bars at the very top and bottom of the page, so a status bar or
  navigation bar that covers them is obvious. The page has no
  StatusBarBehavior of its own, because a behavior re-applies the window
  flags once the modal is visible and hides the bug. The launcher first
  resets the host status bar to exactly Colors.Transparent, since a
  modal copies the status bar of the window it opens from. A switch
  changes the status bar to solid red.
- EdgeToEdgeTitleViewShell is the host window demo (#2351): a Shell with
  a StatusBarBehavior, a TitleView and a TabBar, which restores the
  reproduction that BrokenTitleViewShell had. Both tabs share one view
  model, so the status bar color is the same on both.
- On Android the Shell demo runs in its own Activity, with its own
  Window from App.CreateWindow. Application.OpenWindow opens nothing
  there because MainActivity is SingleTask. Closing the Activity returns
  to the untouched AppShell.
- The TabBar colors are set on the demo Shell, because the Sample's
  Shell style makes the selected tab label almost white on white.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing a modal runs StatusBarBehaviorPage.OnNavigatedTo, which applies
the slider color with full alpha, so the status bar turns opaque black.
The demo button then sets the sliders to 0 again, but they are already
0, so nothing applies a transparent color and the modal copies an
opaque status bar. With main's DialogFragmentService the bug therefore
only showed the first time the demo was opened (API 28: top bar at
y=0, then y=24 on every later open).

The button now also sets the status bar to Colors.Transparent before it
pushes the modal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Android the Toolbar leaves its first 16dp empty, so the amber TitleView
of the Shell demo started 16dp from the left edge and the Shell's blue
toolbar background showed in that strip (seen on an API 33 emulator).

The Shell demo's Activity now uses its own theme, whose toolbar style sets
the start content inset to 0. Only that Activity uses the theme, so the
toolbar of the AppShell keeps its default insets. The theme derives from
Maui.MainTheme.NoActionBar instead of Maui.SplashTheme, because MAUI
replaces the splash theme with the main theme in OnCreate, which would
drop the toolbar style.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The style's name and its zero start insets already say why it exists.
The theme's comment stays, because deriving that theme from
Maui.SplashTheme would silently undo the TitleView fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

API-dependent window behavior needs human validation, and initial modal status-bar color propagation remains unresolved.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

On API 35 and higher, a modal (DialogFragment) did not get the status bar
color of the page that opened it:
- StatusBar.SetColor draws its color in an overlay view on API 35+, not in
  Window.StatusBarColor. So main copied only the theme's color (#141414 in
  the Sample) and this PR copied nothing: a modal opened from a solid red
  status bar showed #141414 (targetSdkVersion 34) or the page background
  (targetSdkVersion 36), on API 35 and API 36 emulators.
- Android still honors Window.SetStatusBarColor on API 35+ for apps that
  target API 34 or lower, and this PR had stopped calling it there. A
  status bar color that such an app sets on its own window was lost: blue
  with main's code, #141414 with this PR's.

The API 35+ path now reads the host window's overlay color through a new
internal StatusBar helper. Apps that target API 34 or lower get that color,
composited over the host window's own status bar color, through
SetStatusBarColor. Apps that target API 35 or higher get the same overlay
as the host, drawn by StatusBar's own code. The paths below API 35 and the
copied light/dark icon appearance are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TheCodeTraveler and others added 3 commits October 16, 2026 09:41
…with

The modal demo only checked the status bar color after its switch, and the
switch calls StatusBar.SetColor on the modal itself, so the demo could not
show whether a modal gets the color of the page that opened it.

A second button, "Modal page demo from a solid red status bar", makes this
page's status bar solid red and opens the same modal. The modal page now
says that its status bar must open with the color of the StatusBarBehavior
page, and its switch starts in that state. The switch is subscribed only
after its initial state is set, so opening the modal never sets the color
itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…under it

53011c1 gave a modal the status bar color of the page that opened it on
API 35+, but two cases went wrong:
- Apps that target API 34 or lower got the host's overlay color baked into
  the modal window's own status bar color. StatusBar.SetColor on the modal
  draws its overlay on top of that color, so a transparent color showed the
  host's color: the solid red demo stayed red after its switch was turned
  off, where f000bc1 showed the window color (#141414).
- The fragment also starts each time the app returns to the foreground, and
  each start copied the host's overlay color over the modal's own. With the
  Sample's targetSdkVersion 36 on API 36, a modal switched to red went back
  to transparent after Home and back, and one switched to transparent went
  back to red. f000bc1 kept the modal's color.

The modal now gets the host window's status bar color and, as an overlay of
its own, the host's overlay color, the same two layers the host page draws.
Android ignores the window color for apps that target API 35 or higher, so
no target SDK check is needed. The overlay is only added when the modal has
none yet, and it is posted: a new window adds the view that draws its own
status bar color on its first layout, and an overlay added before that view
is drawn under it (without the post, a modal opened from the solid red demo
showed #141414 on API 35 with targetSdkVersion 34).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DialogFragmentService handles every DialogFragment, not only modal pages,
and since 53011c1 it added the host's status bar overlay to all of them on
API 35+. A floating DialogFragment, like a Material date or time picker,
does not reach the status bar, so the overlay was drawn at the top of its
own content instead: a solid red demo host gave a floating AlertDialog
fragment a red band over its title, 66 px high on API 35 (the status bar
height resource) and 3 px on API 36 (the window's status bar inset).

Floating dialogs no longer get the overlay, as Android draws no status bar
color for floating windows either. Checked with a scratch floating
DialogFragment that was not committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This comment was marked as outdated.

SA1202 orders members by access; the helpers move unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native-window layout, overlay inheritance, and secondary-activity lifecycle changes need final Android device review.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Resolve status bar overlay from the underlying modal window

src/​CommunityToolkit.Maui.Core/​Services/​DialogFragmentService.android.cs:171

On API 35+, this always copies the activity's overlay, even when another modal opens the new modal. StatusBar.SetColor updates the visible dialog through GetCurrentWindow, leaving the activity's overlay unchanged. For example, open the existing “Try StatusBarBehavior on Modal Page” sample, then launch its solid-red modal demo: the new modal inherits the activity's old color instead of red. Resolve the source from the underlying modal, excluding the dialog being started, and fall back to the activity window.

🧠 Review effort: Balanced

@TheCodeTraveler
TheCodeTraveler enabled auto-merge (squash) October 16, 2026 18:39

@ne0rrmatrix ne0rrmatrix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this PR on emulators (API 33 and API 36). Each scenario was run twice: once with this PR, and once with the same app but with DialogFragmentService.android.cs and StatusBar.android.cs taken from main. In every scenario the opening page sets a solid red status bar first. The test page is here if you want to reproduce it. No scenario crashed.

The fix works: on API 36 a modal page now keeps the opening page's status bar color, and its content stays below the status bar. I found one bug and two behavior changes worth deciding on purpose.

✅ The fix works (API 36)

On main the modal page loses the red status bar. With this PR it keeps it, and the top marker stays below the bar.

Fix works

🐛 1. A nested modal copies the main window's color, not the modal it was opened from (API 35+)

StatusBar.TryGetOverlayColor(hostWindow, ...) always reads activity.Window. Modal A opens red (correct), then sets the status bar to blue. Modal B, pushed from A, opens red, not blue. That contradicts the sample's own rule that a modal "opens with the color of the page that opened it". The fix would be to read the overlay color from the window below the new dialog (the topmost other visible DialogFragment, falling back to activity.Window).

Nested modal

⚠️ 2. Popups and bottom sheets: the status bar is no longer dimmed (API 35+)

Toolkit Popups are modal pages, so they also get the copied overlay. The overlay is added on top of the popup's dim layer, so on API 36 the status bar stays full red while the rest of the screen dims. On main it dims with the screen. The same happens with a Material BottomSheetDialogFragment, because the service handles every non-floating DialogFragment in the app. In fairness, API 33 on main already behaves this way (last screenshot), so this makes API 35+ match older Android. But it is a visible change for API 35+ users, so please make it on purpose and document it. The IsFloating check works: a floating AlertDialog is dimmed correctly with this PR.

Popup and bottom sheet

⚠️ 3. API 30–34: a SafeAreaEdges=None modal now draws under an opaque status bar

SetDecorFitsSystemWindows used to be !isColorTransparent and is now always false. A modal page with default safe areas looks identical before and after this PR. But a modal with SafeAreaEdges="None" used to be kept below an opaque status bar, and now its top is drawn under it. This matches API 35+, but it is a behavior change on API 30–34 that is worth a line in the docs or release notes. It also applies to third-party DialogFragments, because the API 30–34 and below-30 paths have no IsFloating check.

SafeAreaEdges None

Smaller things

  • The two new unit tests only check that a behavior added to a page is in Behaviors and that its default color is Transparent. Neither one runs DialogFragmentService or the overlay code, so the real regression check is the manual sample.
  • See the inline comments for the rest.


// A floating dialog, like a picker, does not reach the status bar, so the overlay would cover its content.
// The fragment also starts each time the app returns to the foreground, and by then the modal page may have set its own color.
if (!dialogWindow.IsFloating && StatusBar.TryGetOverlayColor(hostWindow, out var hostOverlayColor))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hostWindow is always activity.Window, so a modal pushed from another modal copies the activity's overlay color, not the color of the modal underneath it (see screenshot 4 in the review summary). Consider reading the overlay color from the window below this dialog: the topmost other visible DialogFragment, falling back to activity.Window.

This line also adds the copied overlay to Toolkit Popups and to non-MAUI full-screen DialogFragments such as bottom sheets. The overlay draws on top of their dim layer, so the status bar is no longer dimmed (screenshot 2).

}

dialogWindow.SetStatusBarColor(platformColor);
AndroidX.Core.View.WindowCompat.SetDecorFitsSystemWindows(dialogWindow, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be SetDecorFitsSystemWindows(!isColorTransparent). With an opaque color, a modal page with SafeAreaEdges=None now draws its top under the status bar on API 30–34 (screenshot 3 in the review summary). That is consistent with API 35+, but it is a behavior change, and it applies to every DialogFragment in the app because this path has no IsFloating check.

}
else
{
throw new NotSupportedException($"{nameof(DialogFragmentService)} requires Android API 21 or higher");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this branch can never run, because the minimum is API 21. If it ever did, it would throw from inside a fragment lifecycle callback and crash the app. Consider removing the API 21 check, or returning instead of throwing.

{
if (window.DecorView.RootView is ViewGroup decorGroup && decorGroup.FindViewWithTag(statusBarOverlayTag) is null)
{
ApplyStatusBarOverlay(window, platformColor, decorGroup);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this runs in a posted callback, and ApplyStatusBarOverlay creates the view with new View(Activity), which throws if CurrentActivity is null by then. That would be an unhandled crash, though I couldn't trigger it. Using the dialog's own context (window.Context) would avoid relying on that global value.

Behaviors = { statusBarBehavior }
};
var navigationPage = new NavigationPage(new ContentPage());
navigationPage.Navigation.PushModalAsync(modalPage);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PushModalAsync isn't awaited here. Also, neither new test runs DialogFragmentService; they check a behavior's default property values. Fine as smoke tests, but they don't cover #3353.

@ne0rrmatrix ne0rrmatrix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes based on my review above, which has screenshots for each point:

  1. Please fix: a modal pushed from another modal copies the main window's status bar color instead of the color of the modal it was opened from (API 35+, DialogFragmentService.android.cs line 169).
  2. Please decide and document: with this PR, Toolkit Popups (and other full-screen DialogFragments) no longer dim the status bar on API 35+. If that's intended, a note in the docs is enough. If not, the overlay should not be added to the popup's window, or it should go under the dim layer.

The API 30–34 SafeAreaEdges=None change and the smaller items are not blocking.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants