Skip to content

feat(Android, Stack v5): expose statusBarScrimColor for the header - #4533

Open
kligarski wants to merge 3 commits into
@kligarski/stack-v5-android-header-backgroundfrom
@kligarski/stack-v5-android-header-status-bar-scrim
Open

feat(Android, Stack v5): expose statusBarScrimColor for the header#4533
kligarski wants to merge 3 commits into
@kligarski/stack-v5-android-header-backgroundfrom
@kligarski/stack-v5-android-header-status-bar-scrim

Conversation

@kligarski

@kligarski kligarski commented Aug 20, 2026

Copy link
Copy Markdown
Member

Description

In edge-to-edge apps the stack header draws under the status bar. Whenever
scroll flags let header content leave the screen, the title and buttons pass
straight through the status-bar area with nothing masking them. This PR exposes
statusBarScrimColor prop and enables a sensible scrim by default, so status
bar is always displayed on a solid background instead of obstructing the
toolbar.

Closes https://github.com/software-mansion/react-native-screens-labs/issues/905.

Details

Material provides two independent status-bar scrim mechanisms, depending on
header type.

Small header — AppBarLayout.statusBarForeground

The small header is an AppBarLayout (ABL) hosting a MaterialToolbar. ABL can
draw a statusBarForeground drawable after (above) all of its children, sized
to the full width × the top window inset and translated against the scroll
offset — so the strip stays glued to the top of the window while the toolbar
scrolls away underneath it.

We want the default strip to always match the bar's effective background
color, including mid-flight during the lift-on-scroll animation (the background
animates between backgroundColor and its blend with
scrolledBackgroundColor). Material has a built-in sync for this, but it is
narrowly keyed: when a drawable is set as statusBarForeground, ABL captures
its color (possible only for ColorDrawable, MaterialShapeDrawable and
ColorStateListDrawable) and re-tints the strip on each animation frame only
if the captured color equals the theme's colorSurface
. Any custom background
color breaks the sync (the strip freezes while the bar animates).

So instead we install a PaintDrawable as the strip — it is not
color-extractable, the captured color is null, and the built-in sync is
permanently inert regardless of theme — and drive the tint ourselves from the
public AppBarLayout.LiftOnScrollProgressListener. That listener is invoked
from the same ValueAnimator frame with the same per-frame mixed color the
built-in sync would apply, so the result is pixel-identical to the native effect
while working for arbitrary colors. The lift animation only runs on lifted-state
changes, so when color props change while the bar is already lifted, the
applicator jumps the strip straight to the blended end color (same pattern as
the existing background jump).

Medium / large headers — CollapsingToolbarLayout.statusBarScrim

Collapsing headers pair the ABL with a CollapsingToolbarLayout (CTL), which
has its own mechanism: statusBarScrim. It shares the alpha animation of the
content scrim (fades in when the header collapses, out when it expands) and is
drawn after all children and after the collapsing title — unlike the content
scrim, which is drawn below the toolbar child and therefore cannot mask
toolbar content.

The default matters here: with scroll-only flags (no exitUntilCollapsed) the
CTL exits the screen but leaves the toolbar behind the status-bar area.
statusBarScrim property can be used to mask it. We default it to the content
scrim color (scrolledBackgroundColor), which makes the masking seamless and is
a visual no-op in the common opaque exitUntilCollapsed configuration.

One artifact needed a workaround: both CTL scrims share the same scrimAlpha
and overlap in the status-bar strip, so mid-fade the strip composites two layers
at partial alpha — visibly darker than either scrim duration of the fade. The
fix removes the overlap: our content scrim (the bottom layer) is a small
ColorDrawable subclass that skips the strip band whenever a status bar scrim
is installed. The band is exactly [-offset, -offset + topInset] in CTL
coordinates — the same rect CTL assigns to the status bar scrim — and the
content scrim's visible top edge always aligns with the strip top, so the
exclusion is a single clip. The offset is tracked with an
OnOffsetChangedListener and the inset by observing dispatchApplyWindowInsets
(both ABL and CTL install their own OnApplyWindowInsetsListener, so setting
ours would silently replace Material's).

Defaults and policy

Header statusBarScrimColor unset Explicit color
small constant strip following the bar's effective background color through the lift animation static strip of that color
medium / large scrim in the content scrim color, fading with the collapse that color, same fade
  • The default scrim is installed only when the color it follows resolves to a
    fully opaque color — translucent headers stay see-through instead of
    getting a double-composited band. An explicit color is always honored.
  • 'transparent' disables the scrim entirely.
  • The small strip is constant while the medium/large scrim shows only
    when collapsed — this asymmetry mirrors the native Material widgets.

Changes

  • Added the statusBarScrimColor prop (fabric spec, public Android types,
    config plumbing, view manager) reusing the BACKGROUND_COLORS
    invalidation flag.
  • StackHeaderAppBarLayout.Small: installed a PaintDrawable status bar
    strip and a LiftOnScrollProgressListener keeping it in sync with the
    bar's effective color; the listener is detached when an explicit color is
    set.
  • StackHeaderApplicator: new applyStatusBarScrim step dispatching per
    header type, with the opacity policy in a single
    resolveStatusBarScrimColor helper; invisible collapsing scrims are
    normalized to null.
  • StackHeaderContentScrimDrawable + exclusion plumbing in
    StackHeaderAppBarLayout.Collapsing: the content scrim skips the
    status-bar strip while a status bar scrim is installed, so the two fading
    scrims never stack.
  • Added the test-stack-header-status-bar-scrim-android SFT.

Visual documentation

4533.mp4

Test plan

Run test-stack-header-status-bar-scrim-android,
test-stack-header-background-android.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

Stack created with GitHub Stacks CLIGive Feedback 💬

@kligarski
kligarski requested a balanced review from Copilot August 20, 2026 14:05
@kligarski
kligarski marked this pull request as ready for review August 20, 2026 14:08

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.

Pull request overview

Adds configurable status-bar scrims to Android Stack v5 headers for edge-to-edge layouts.

Changes:

  • Exposes and documents statusBarScrimColor.
  • Implements scrims for small and collapsing headers.
  • Adds a manual single-feature test scenario.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/fabric/stack/StackHeaderConfigAndroidNativeComponent.ts Adds the native prop specification.
src/components/stack/header/StackHeaderConfig.android.types.ts Documents the public Android API.
apps/.../scenario.md Defines manual visual checks.
apps/.../scenario-description.ts Registers scenario metadata.
apps/.../index.tsx Implements the interactive test screen.
apps/src/tests/single-feature-tests/stack-v5/index.ts Registers the new scenario.
android/.../StackHeaderApplicator.kt Resolves and applies scrim colors.
android/.../StackHeaderConfigViewManager.kt Receives the Fabric prop.
android/.../StackHeaderConfigurationProviding.kt Extends the configuration contract.
android/.../StackHeaderConfig.kt Stores and invalidates the prop.
android/.../StackHeaderContentScrimDrawable.kt Prevents overlapping scrim rendering.
android/.../StackHeaderAppBarLayout.kt Adds scrim synchronization and inset tracking.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@kligarski
kligarski force-pushed the @kligarski/stack-v5-android-header-status-bar-scrim branch 2 times, most recently from 6e0cd9f to f70e89b Compare August 21, 2026 12:21
@kligarski
kligarski force-pushed the @kligarski/stack-v5-android-header-status-bar-scrim branch from f70e89b to 262b0ac Compare August 21, 2026 14:37
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bc15b313-ff08-40c5-89d5-7ec035474d30

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kligarski
kligarski force-pushed the @kligarski/stack-v5-android-header-status-bar-scrim branch from 262b0ac to 5d816ac Compare August 21, 2026 14:43
kligarski and others added 3 commits August 21, 2026 16:44
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kligarski
kligarski force-pushed the @kligarski/stack-v5-android-header-status-bar-scrim branch from 5d816ac to efba05d Compare August 21, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants