Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
[.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android #37137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: net11.0
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
[.NET 11] [Shell] Fix gradient background not rendering on bottom tab bar on iOS and Android #37137
Changes from all commits
f9b30767d7104c3d62979File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
There are no files selected for viewing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Architectural Layer Placement — The precedence chain (
TabBarBackgroundColor→Shell.Background→Shell.BackgroundColor→ default) is reconstructed by hand here and byte-for-byte duplicated inSafeShellTabBarAppearanceTracker.cs:74-80. Because it was removed fromShellAppearance(the shared layer) and re-added per platform, Windows/Tizen/ShellItemRenderersilently diverge (see theShellAppearance.cs:82comment). This resolution belongs onShellAppearanceas an internalEffectiveTabBarBackgroundbrush so all platforms share one definition.Also,
appearance as ShellAppearancenarrows anIShellAppearanceElementparameter to a concrete type: any otherIShellAppearanceElementimplementation now yieldsshellAppearance == null, so both theBackgroundandBackgroundColorfallbacks silently disappear for it (previously the interface member supplied theBackgroundColorfallback for everyone).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Performance-Critical Path / Native Platform Defaults Preservation — The gradient branch returns immediately, so unlike the solid path it has no "same value → early return" short-circuit.
SetAppearanceruns on every Shell appearance change (including tab/section switches), so a static gradient re-runsUpdateBackgroundand rebuilds the drawable on every tab switch. Cache the last applied brush and skip when unchanged, mirroring thelastColor == newColorguards below.It also bypasses the
MaterialShapeDrawable+InitializeElevationOverlay(bottomView.Context)path used for solid colors, so a gradient tab bar loses the Material elevation overlay in dark theme. If that is intentional, a short comment would prevent a future "fix" from re-adding it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Logic and Correctness / Memory — Gradient → solid transition is not handled cleanly. After the gradient branch has installed a
MauiDrawableviabottomView.UpdateBackground(brush), a subsequent solid/reset call reaches here whereoldBackground as ColorDrawableandas ColorChangeRevealDrawableare both null, solastColorfalls back toColors.Transparentand theColorChangeRevealDrawableanimates from transparent — a visible flash rather than a gradient→color transition. The previousMauiDrawableis also replaced byViewCompat.SetBackgroundwithout being disposed (contrast withViewExtensions.UpdateBackground, which disposes the outgoingMauiDrawable). Detect the gradient-drawable case explicitly and dispose/short-circuit the reveal animation.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Backward Compatibility / Public API Surface —
protected virtual SetBackgroundColoris a shipped API (PublicAPI/net-android/PublicAPI.Shipped.txt:4369). After this change no call site invokes it any more (ResetAppearanceline 45 andSetAppearanceline 81 both callSetBackgrounddirectly), so every existing subclass that overridesSetBackgroundColorto customize the bottom-bar background is silently bypassed — it compiles, emits only an obsolete warning, and stops taking effect at runtime. That is a behavioral break for third-party Shell renderers shipping against a servicing branch (net11.0).Suggested shape: keep
SetBackgroundhandling only the gradient case and delegate the solid/null case to the existingSetBackgroundColorvirtual (wrapped in#pragma warning disable CS0618), so subclass overrides continue to be dispatched. That also avoids obsoleting a shipped protected member and avoids adding new permanent protected API for a bug fix.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Backward Compatibility —
SetBackgroundColoris a shippedprotected virtualextension point, andSetAppearance/ResetAppearanceno longer call it. Any existing subclass that overridesSetBackgroundColorto customize the bottom bar background silently stops being invoked after this change — the override compiles, produces only an obsoletion warning, and the customization vanishes at runtime. Consider havingSetBackgroundroute the non-gradient case throughSetBackgroundColor(e.g.#pragma-suppressed call to the obsolete member) for one release so overrides keep working, or call this out explicitly as a breaking change in the PR description/release notes.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Architectural Layer Placement — This 4-level precedence ternary is a verbatim copy of the one added in
ShellBottomNavViewAppearanceTracker.SetAppearance(Android, line 56). Because the precedence now lives in the platform trackers rather than inShellAppearance, the pre-iOS15 branch below (UpdateTabBarAppearance, line 114) and the Windows/Tizen handlers keep using the rawEffectiveTabBarBackgroundColorand get a different answer for the same Shell state. Resolve the precedence once inShellAppearance(see comment on ShellAppearance.cs) and have every tracker consume it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[critical] Logic and Correctness —
Color.AsPaint()is an instance method onColor(src/Graphics/src/Graphics/Color.cs:166), not a null-tolerant extension method, sotabbed.BarBackgroundColor.AsPaint()throwsNullReferenceExceptionwheneverBarBackgroundColorisnullwhileIsSet(BarBackgroundColorProperty)istrue.IsSetis true for any explicit assignment, including an explicit null. Concrete repro:tabbed.BarBackgroundColor = Colors.Red; tabbed.BarBackgroundColor = null;(orBarBackgroundColor="{Binding BarColor}"/{AppThemeBinding Light=..., Dark={x:Null}}resolving to null) → crash on the next appearance update. Before this change the same input producedbarBackgroundColor == nulland fell back todefaultBarColor.Suggest
tabbed.BarBackgroundColor?.AsPaint()(and let the existingnull => defaultBarColorswitch arm handle it).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[critical] Logic and Correctness — Same NRE as
TabbedPage.iOS.cs:849:Color.AsPaint()is an instance method, sotabbed.BarBackgroundColor.AsPaint()throws whenBarBackgroundColorPropertyis set to an explicitnull(clearing the bar color, or a binding resolving to null). The previous code passednulland correctly restored_defaultBarColor. Use?.AsPaint().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Public API Surface Design — This adds a permanent
protected virtualAPI to a public unsealed compatibility type on a servicing branch (net11.0) in order to fix a rendering bug, while simultaneously obsoleting its shipped predecessor (PublicAPI.Shipped.txt:4369). Protected virtuals onShellBottomNavViewAppearanceTrackercannot be removed later and become part of the compatibility-renderer contract.If the gradient handling can be done inside the existing
SetBackgroundColordispatch path (see theShellBottomNavViewAppearanceTracker.cs:148comment), this API addition — and the obsoletion warning it forces on every existing subclass — can be avoided entirely.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[critical] Cross-Platform Behavioral Consistency / Regression Prevention — Dropping the
?? BackgroundColorfallback changes the semantics of theIShellAppearanceElement.EffectiveTabBarBackgroundColorinterface contract for every consumer, but only two of the five call sites were compensated in this PR.Uncompensated consumers that lose the
Shell.BackgroundColor→ tab bar fallback:src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Windows.cs:763→UpdateTopNavAreaBackground(null); Windows top-nav area reverts to the platform default when onlyShell.BackgroundColoris set.src/Controls/src/Core/Handlers/Shell/ShellItemHandler.Tizen.cs:106.src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs:149— now always writesDefaultBottomNavigationViewBackgroundColorinto the existingColorDrawable.src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs:114(UpdateTabBarAppearance, the pre-iOS-15 path) still readsEffectiveTabBarBackgroundColordirectly and was not updated alongside the iOS-15 path at line 74.Repro: a Shell with only
Shell.BackgroundColor="Red"(noTabBarBackgroundColor, noShell.Background) → red tab bar before, default tab bar after, on Windows/Tizen.Prefer leaving
EffectiveTabBarBackgroundColoruntouched and adding a newEffectiveTabBarBackground(Brush) member that encodes the full precedence, so no existing consumer changes behavior.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Regression Prevention / Cross-Platform Consistency — Dropping the
?? BackgroundColorfallback here changes behavior for every consumer ofIShellAppearanceElement.EffectiveTabBarBackgroundColor, but the newTabBarBackgroundColor > Background > BackgroundColorprecedence chain was only re-implemented in two of them (AndroidShellBottomNavViewAppearanceTracker.SetAppearanceand the iOS 15+ path ofSafeShellTabBarAppearanceTracker). The remaining consumers now silently lose theShell.BackgroundColorfallback with nothing replacing it:Handlers/Shell/ShellItemHandler.Windows.cs:763—Shell.BackgroundColorno longer tints the top nav/tab area on Windows.Handlers/Shell/ShellItemHandler.Tizen.cs:106.Compatibility/Handlers/Shell/iOS/SafeShellTabBarAppearanceTracker.cs:114(UpdateTabBarAppearance, the pre-iOS15 path) and:30.Compatibility/Handlers/Shell/Android/ShellItemRenderer.cs:149, which now paints the bottom view withDefaultBottomNavigationViewBackgroundColorwhen onlyShell.BackgroundColoris set.Prefer exposing the resolved value on
ShellAppearanceitself (e.g. an internalEffectiveTabBarBackgroundbrush implementing the full precedence) so all platforms share one implementation, instead of duplicating the ternary chain per-platform and regressing the untouched platforms.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[critical] Logic and Correctness — Same
NullReferenceExceptionasTabbedRenderer.cs:645.Color.AsPaint()is an instance method (src/Graphics/src/Graphics/Color.cs:166);IsSet(BarBackgroundColorProperty)istruefor an explicitly-assignednull, soBarBackgroundColor.AsPaint()will NRE forBarBackgroundColor = nullafter a previous non-null assignment, or for a binding/AppThemeBindingthat resolves to null.Use
BarBackgroundColor?.AsPaint().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[critical] Logic and Correctness —
Color.AsPaint()is an instance method onMicrosoft.Maui.Graphics.Color(Color.cs:166), not a null-safe extension.IsSet(BarBackgroundColorProperty)is true whenever the property has been assigned, including an explicitnull(tabbedPage.BarBackgroundColor = null, or a binding that resolves to null after previously resolving to a color). In that caseBarBackgroundColor.AsPaint()throwsNullReferenceExceptionand the tab bar update crashes. Before this change the same expression passednullthrough safely andUpdateiOS15TabBarAppearancefell back todefaultBarColor, restoring the platform default. UseBarBackgroundColor?.AsPaint()(and see the related stale-color issue in TabbedViewExtensions.cs:48).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Regression Prevention and Test Coverage — This host page is shared by every UI-test leg, and adding a second bottom tab changes the rendering on all of them, but only 2 of the 5 committed baselines for
ShellBackgroundSupportsGradientBrushwere regenerated:Updated:
TestCases.iOS.Tests/snapshots/ios/,TestCases.iOS.Tests/snapshots/ios-26/.Not updated (will fail):
TestCases.Android.Tests/snapshots/android/,TestCases.Mac.Tests/snapshots/mac/,TestCases.WinUI.Tests/snapshots/windows/.The try-fix run already observed an 8.58% Android screenshot delta from exactly this host-page change (
try-fix/content.md, candidate 1). Regenerate the Android, Mac, and Windows baselines, or limit the new host layout to platforms with updated baselines.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Regression Prevention — Switching
AddContentPagetoAddBottomTab(plus the new "Settings" tab) changes the rendered UI ofIssue10445on every platform, but only the iOS baselines (TestCases.iOS.Tests/snapshots/iosandios-26) were regenerated. The existing baselinesTestCases.Android.Tests/snapshots/android/ShellBackgroundSupportsGradientBrush.png,TestCases.WinUI.Tests/snapshots/windows/...andTestCases.Mac.Tests/snapshots/mac/...still show the old single-page layout, soShellBackgroundSupportsGradientBrushwill fail on Android/Windows/MacCatalyst. Regenerate all baselines — the Android one in particular is the only coverage for theShellBottomNavViewAppearanceTrackerhalf of this fix.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Regression Prevention and Test Coverage — Gate reported no new tests, and the only coverage added is a visual tweak to an existing screenshot test. The behaviors this PR actually changes are untested:
Shell.TabBarBackgroundColormust still win over a gradientShell.Background(the new nested conditional inShellBottomNavViewAppearanceTracker.cs:56-61andSafeShellTabBarAppearanceTracker.cs:74-80).EffectiveTabBarBackgroundColor→BackgroundColorfallback: a Shell with onlyShell.BackgroundColorset must still tint the tab bar (a device/unit test here would have caught the Windows/Tizen/ShellItemRendererregression).TabbedViewExtensions.cs:57comment).A
ShellAppearance-level unit test inControls.Core.UnitTestscovering the precedence matrix would be cheap and would pin the contract that the platform trackers now duplicate.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Regression Prevention — Coverage is limited to a single static screenshot of the "gradient wins" state. None of the transitions this PR actually changes are exercised: (a)
TabBarBackgroundColorset → gradient must be ignored (precedence), (b) gradient → solid/default (ResetAppearance, iOSelsebranch at TabbedViewExtensions.cs:64), (c)TabbedPage.BarBackgroundColorset then cleared tonull— which currently NREs on iOS (see TabbedPage.iOS.cs:849), and (d) tab re-selection/re-navigation re-runningSetAppearance. At minimum add a case that toggles the background back to solid/null after the gradient, since that is where the stale-appearance and null-color bugs live.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Logic and Correctness — The
switchloses the previous "no explicit background → use the platform default" behavior for two inputs:SolidPaintwith anullColor→solidPaint.Color?.ToPlatform()yieldsnull, anddefaultBarColoris never applied. PreviouslybarBackgroundColor == nullmapped straight todefaultBarColor. This is reachable today viaBarBackgroundColor.AsPaint()atTabbedRenderer.cs:645/TabbedPage.iOS.cs:849(see the NRE comments there — once null-guarded, this arm becomes the live path).Paint(e.g.ImagePaint) →_ => null, and neither the gradient branch nor the color branch runs, so the tab bar keeps whatever appearance it had.Consider
SolidPaint { Color: null } => defaultBarColorand an explicit fallback for unsupported paint types.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Logic and Correctness — The
_ => nullarm (andSolidPaint solidPaint => solidPaint.Color?.ToPlatform()whenColoris null) makeseffectiveBarColornull, which theif (effectiveBarColor != null)guard at line 68 treats as "do nothing" — not as "restoredefaultBarColor". Two reachable regressions:Shell.Background = new ImageBrush{...}converts toImageSourcePaint, hits_ => null, so no background is applied at all AND_tabBarAppearance.BackgroundColorkeeps the previously applied color (the appearance object is cached in thereffield across calls). Android'sUpdateBackgrounddoes render image paints, so the platforms diverge.SolidColorBrushwith a nullColor, orBarBackgroundColor = nullwhileIsSetis true) leaves the stale color on the tab bar instead of reverting to the platform default.Make the fallback explicit:
SolidPaint sp => sp.Color?.ToPlatform() ?? defaultBarColor, and_ => defaultBarColorfor non-gradient/unsupported paints (or handle them viatabBar.UpdateBackground).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Handler Mapper and Property Patterns / Cross-Platform Consistency — The gradient branch mutates
_tabBarAppearance.BackgroundEffect = nullandBackgroundColor = UIColor.Clear, but nothing restores those when the app later transitions gradient → solid/default._tabBarAppearanceis a cachedreffield reused on the next call: taking theelsebranch removes the gradient layer, and ifeffectiveBarColoris null (default) the appearance is left withBackgroundEffect == null+BackgroundColor == UIColor.Clear, i.e. a transparent tab bar with no blur instead of the restored system default. Re-runConfigureWithDefaultBackground()(or re-apply the captureddefaultBarColor/effect) in the non-gradient branch. NoteRemoveBackgroundLayer()is also called unconditionally on every appearance update, including the common no-gradient case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Native Platform Defaults Preservation — The gradient branch mutates
_tabBarAppearance.BackgroundEffect = null,_tabBarAppearance.BackgroundColor = UIColor.ClearandtabBar.BackgroundColor = UIColor.Clear, but none of these is ever restored.ConfigureWithDefaultBackground()only runs once, when_tabBarAppearanceis first allocated (line 42), and theelsebranch (line 64) only callsRemoveBackgroundLayer().Repro: set a gradient
Shell.Background/BarBackground, then clear it (or navigate to a page whose appearance has no background).barBackgroundbecomesnull→effectiveBarColor = defaultBarColor; ifdefaultBarColorisnull(the Shell tracker passesnullfordefaultBarColor—SafeShellTabBarAppearanceTracker.cs:87), theif (effectiveBarColor != null)block is skipped and the tab bar is left permanently transparent with the default blur effect destroyed.The
elsebranch should also restore the default background (_tabBarAppearance.ConfigureWithDefaultBackground()or a captured default) alongsideRemoveBackgroundLayer().Uh oh!
There was an error while loading. Please reload this page.