From 5749f903ef13b98b38ac3e7b73aa4bb093d3a3ec Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Wed, 19 Aug 2026 13:45:14 +0200 Subject: [PATCH 1/6] add backgroundColor and scrollableBackgroundColor props --- .../stack/header/StackHeaderApplicator.kt | 26 ++++++++++++ .../header/StackHeaderCoordinatorLayout.kt | 5 +++ .../stack/header/config/StackHeaderConfig.kt | 6 +++ .../config/StackHeaderConfigViewManager.kt | 14 +++++++ .../StackHeaderConfigurationProviding.kt | 2 + .../config/StackHeaderInvalidationFlags.kt | 3 +- .../header/StackHeaderConfig.android.types.ts | 40 ++++++++++++++++++- ...StackHeaderConfigAndroidNativeComponent.ts | 3 ++ 8 files changed, 97 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt index a93fde9899..afb8160de0 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt @@ -16,6 +16,7 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.Toolbar import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.graphics.drawable.toDrawable import com.google.android.material.R import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS @@ -33,6 +34,7 @@ import com.swmansion.rnscreens.stack.header.config.StackHeaderConfigurationProvi import com.swmansion.rnscreens.stack.header.config.StackHeaderType import com.swmansion.rnscreens.stack.header.subview.StackHeaderSubview import com.swmansion.rnscreens.utils.dpToPx +import com.swmansion.rnscreens.utils.resolveColorAttr import com.swmansion.rnscreens.utils.resolveDrawableAttr import com.swmansion.rnscreens.utils.spToPx import kotlin.math.roundToInt @@ -455,6 +457,30 @@ internal class StackHeaderApplicator( appBar.requestLayout() } + internal fun applyBackgroundColors( + appBar: StackHeaderAppBarLayout, + config: StackHeaderConfigurationProviding, + ) { + val backgroundColor = + config.backgroundColor ?: resolveColorAttr(appBar.context, R.attr.colorSurface) + val scrolledBackgroundColor = + config.scrolledBackgroundColor + ?: resolveColorAttr(appBar.context, R.attr.colorSurfaceContainer) + + when (appBar) { + is StackHeaderAppBarLayout.Small -> { + appBar.background = backgroundColor.toDrawable() + appBar.setLiftOnScrollColor(ColorStateList.valueOf(scrolledBackgroundColor)) + } + + is StackHeaderAppBarLayout.Collapsing -> { + appBar.setLiftOnScrollColor(null) + appBar.background = backgroundColor.toDrawable() + appBar.collapsingToolbarLayout.setContentScrimColor(scrolledBackgroundColor) + } + } + } + // endregion // region Helpers diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderCoordinatorLayout.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderCoordinatorLayout.kt index b92daaba76..dbcf9814d0 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderCoordinatorLayout.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderCoordinatorLayout.kt @@ -226,6 +226,11 @@ internal class StackHeaderCoordinatorLayout( provider.clearInvalidationFlags(StackHeaderInvalidationFlags.SCROLL_FLAGS) } + if (needsRebuild || provider.invalidationFlags.containsAny(StackHeaderInvalidationFlags.BACKGROUND_COLORS)) { + applicator.applyBackgroundColors(appBar, provider) + provider.clearInvalidationFlags(StackHeaderInvalidationFlags.BACKGROUND_COLORS) + } + if (needsRebuild || provider.invalidationFlags.containsAny(StackHeaderInvalidationFlags.LIFT_ON_SCROLL)) { // Lift-on-scroll is disabled in transparent mode: there is no content // scrolling behavior installed and the app bar overlays the content. diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfig.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfig.kt index 718d4caf4a..fb2ec714bb 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfig.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfig.kt @@ -137,6 +137,12 @@ internal class StackHeaderConfig( override var liftOnScroll: Boolean by invalidatingProperty(true, StackHeaderInvalidationFlags.LIFT_ON_SCROLL) internal set + override var backgroundColor: Int? by invalidatingProperty(null, StackHeaderInvalidationFlags.BACKGROUND_COLORS) + internal set + + override var scrolledBackgroundColor: Int? by invalidatingProperty(null, StackHeaderInvalidationFlags.BACKGROUND_COLORS) + internal set + override var toolbarMenu: StackHeaderToolbarMenuConfig by invalidatingProperty(StackHeaderToolbarMenuConfig(emptyList(), emptyList()), StackHeaderInvalidationFlags.TOOLBAR_MENU) internal set diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigViewManager.kt index 43b3bfb08d..7c9dab95aa 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigViewManager.kt @@ -566,6 +566,20 @@ internal open class StackHeaderConfigViewManager : view.liftOnScroll = value } + override fun setBackgroundColor( + view: StackHeaderConfig, + value: Int?, + ) { + view.backgroundColor = value + } + + override fun setScrolledBackgroundColor( + view: StackHeaderConfig, + value: Int?, + ) { + view.scrolledBackgroundColor = value + } + override fun setToolbarMenuGroupDividerEnabled( view: StackHeaderConfig, value: Boolean, diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigurationProviding.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigurationProviding.kt index 142ff9b145..cd0c2265bb 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigurationProviding.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderConfigurationProviding.kt @@ -27,6 +27,8 @@ internal interface StackHeaderConfigurationProviding { val scrollFlagExitUntilCollapsed: Boolean val scrollFlagSnap: Boolean val liftOnScroll: Boolean + val backgroundColor: Int? + val scrolledBackgroundColor: Int? val leadingSubview: StackHeaderSubviewProviding? val centerSubview: StackHeaderSubviewProviding? val trailingSubview: StackHeaderSubviewProviding? diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderInvalidationFlags.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderInvalidationFlags.kt index 0b64998e5e..fcd61a018f 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderInvalidationFlags.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/config/StackHeaderInvalidationFlags.kt @@ -17,8 +17,9 @@ internal value class StackHeaderInvalidationFlags( val TITLE_POSITIONING = StackHeaderInvalidationFlags(1 shl 8) val TITLE_APPEARANCE = StackHeaderInvalidationFlags(1 shl 9) val CONTENT_INSETS = StackHeaderInvalidationFlags(1 shl 10) + val BACKGROUND_COLORS = StackHeaderInvalidationFlags(1 shl 11) val APPEARANCE = - TITLE or BACK_BUTTON or OVERFLOW_ICON or TITLE_POSITIONING or TITLE_APPEARANCE or CONTENT_INSETS + TITLE or BACK_BUTTON or OVERFLOW_ICON or TITLE_POSITIONING or TITLE_APPEARANCE or CONTENT_INSETS or BACKGROUND_COLORS val ALL = STRUCTURE or SUBVIEWS or APPEARANCE or SCROLL_FLAGS or TOOLBAR_MENU or LIFT_ON_SCROLL } diff --git a/src/components/stack/header/StackHeaderConfig.android.types.ts b/src/components/stack/header/StackHeaderConfig.android.types.ts index 3f21c71385..e013eb06d3 100644 --- a/src/components/stack/header/StackHeaderConfig.android.types.ts +++ b/src/components/stack/header/StackHeaderConfig.android.types.ts @@ -745,7 +745,8 @@ export interface StackHeaderConfigPropsAndroid { * Material `CollapsingToolbarLayout` uses a fade title-collapse mode that * installs its own content scrim and disables the app bar's lift-on-scroll, * so this prop has no effect there. The collapsed appearance of those headers - * is instead controlled by that content scrim, which is not exposed yet. + * is instead controlled by that content scrim — see + * {@link scrolledBackgroundColor}. * * Has no effect while the header is `transparent` (there is no scrolling * content behavior installed in that mode). @@ -755,6 +756,43 @@ export interface StackHeaderConfigPropsAndroid { * @platform android */ liftOnScroll?: boolean | undefined; + /** + * @summary Background color of the header. + * + * @description + * Applies to all header types. For `medium` / `large` headers this is the + * color of the expanded state — the collapsed state color is controlled by + * {@link scrolledBackgroundColor}. + * + * @remarks + * If value is not provided, falls back to Material's default. + * + * @platform android + */ + backgroundColor?: ColorValue | undefined; + /** + * @summary Background color of the header when content is scrolled beneath + * it. + * + * @description + * For the `small` header, this is the lift-on-scroll target color: when + * content is scrolled beneath the app bar, the background animates from + * {@link backgroundColor} to this color. Requires {@link liftOnScroll}. + * + * For `medium` / `large` headers, this is the color of the content scrim + * that fades in as the header collapses. The scrim is drawn above the + * header background (and the `backgroundSubview`, if any) but below the + * toolbar content. + * + * @remarks + * A translucent color is composited over the header background instead of + * replacing it. + * + * If value is not provided, falls back to Material's default. + * + * @platform android + */ + scrolledBackgroundColor?: ColorValue | undefined; /** * @summary Toolbar menu configuration. * diff --git a/src/fabric/stack/StackHeaderConfigAndroidNativeComponent.ts b/src/fabric/stack/StackHeaderConfigAndroidNativeComponent.ts index dc83ac551b..fdc61a760d 100644 --- a/src/fabric/stack/StackHeaderConfigAndroidNativeComponent.ts +++ b/src/fabric/stack/StackHeaderConfigAndroidNativeComponent.ts @@ -179,6 +179,9 @@ export interface NativeProps extends ViewProps { liftOnScroll?: CT.WithDefault; + backgroundColor?: ColorValue | undefined; + scrolledBackgroundColor?: ColorValue | undefined; + toolbarMenu?: UnsafeMixed | undefined; toolbarMenuGroupDividerEnabled?: CT.WithDefault; onToolbarMenuItemPress?: From 56ac92e8f9fac3ae2e80857a11bfafd4557bfc4d Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Wed, 19 Aug 2026 14:19:13 +0200 Subject: [PATCH 2/6] handle runtime changes when small header is lifted --- .../stack/header/StackHeaderApplicator.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt index afb8160de0..8bb812a216 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt @@ -26,6 +26,8 @@ import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.color.MaterialColors +import com.google.android.material.shape.MaterialShapeDrawable import com.swmansion.rnscreens.common.text.TextAppearance import com.swmansion.rnscreens.common.text.TextAppearanceDefaults import com.swmansion.rnscreens.ext.detachFromCurrentParent @@ -471,6 +473,18 @@ internal class StackHeaderApplicator( is StackHeaderAppBarLayout.Small -> { appBar.background = backgroundColor.toDrawable() appBar.setLiftOnScrollColor(ColorStateList.valueOf(scrolledBackgroundColor)) + + // The lift animation runs only on lifted-state changes; jump to the end + // state when colors change while already lifted. The end state is the + // scrolled color composited over the background (see Material's + // initializeLiftOnScrollWithColor), not the raw scrolled color — they + // differ when the scrolled color is not fully opaque. + if (appBar.isLifted) { + (appBar.background as? MaterialShapeDrawable)?.fillColor = + ColorStateList.valueOf( + MaterialColors.layer(backgroundColor, scrolledBackgroundColor), + ) + } } is StackHeaderAppBarLayout.Collapsing -> { From bd750b988aca644e5088f82530ae6d364d474d7a Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Wed, 19 Aug 2026 14:19:29 +0200 Subject: [PATCH 3/6] add SFT --- .../single-feature-tests/stack-v5/index.ts | 3 + .../index.tsx | 242 ++++++++++++++++++ .../scenario-description.ts | 11 + .../scenario.md | 127 +++++++++ 4 files changed, 383 insertions(+) create mode 100644 apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/index.tsx create mode 100644 apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario-description.ts create mode 100644 apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario.md diff --git a/apps/src/tests/single-feature-tests/stack-v5/index.ts b/apps/src/tests/single-feature-tests/stack-v5/index.ts index 39d68846ff..95b2b02ba3 100644 --- a/apps/src/tests/single-feature-tests/stack-v5/index.ts +++ b/apps/src/tests/single-feature-tests/stack-v5/index.ts @@ -29,6 +29,7 @@ import TestStackHeaderMenuOptionsIOS from './test-stack-header-menu-options-ios' import TestStackHeaderItemIdentifierIOS from './test-stack-header-item-identifier-ios'; import TestStackHeaderTitleAppearance from './test-stack-header-title-appearance-android'; import TestStackHeaderContentInsets from './test-stack-header-content-insets-android'; +import TestStackHeaderBackground from './test-stack-header-background-android'; // Scenario entry-point components — each scenario's default export re-exported // under a name for direct rendering (e.g. from App.tsx or e2e harnesses). @@ -59,6 +60,7 @@ export { default as TestStackToolbarMenuBatchCommands } from './test-stack-toolb export { default as TestStackToolbarMenuA11y } from './test-stack-toolbar-menu-a11y-android'; export { default as TestStackHeaderTitleAppearance } from './test-stack-header-title-appearance-android'; export { default as TestStackHeaderContentInsets } from './test-stack-header-content-insets-android'; +export { default as TestStackHeaderBackground } from './test-stack-header-background-android'; const scenarios = { TestStackPreventNativeDismissSingleStack, @@ -88,6 +90,7 @@ const scenarios = { TestStackToolbarMenuA11y, TestStackHeaderTitleAppearance, TestStackHeaderContentInsets, + TestStackHeaderBackground, }; const StackScenarioGroup: ScenarioGroup = { diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/index.tsx b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/index.tsx new file mode 100644 index 0000000000..b71155cf75 --- /dev/null +++ b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/index.tsx @@ -0,0 +1,242 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + Image, + PlatformColor, + ScrollView, + StyleSheet, + Text, + View, + type ColorValue, +} from 'react-native'; +import { scenarioDescription } from './scenario-description'; +import { createScenario } from '@apps/tests/shared/helpers'; +import { + StackContainer, + useStackNavigationContext, +} from '@apps/shared/containers/stack'; +import { SettingsPicker, SettingsSwitch } from '@apps/shared'; +import { Colors } from '@apps/shared/styling'; +import LongText from '@apps/shared/LongText'; +import { + type StackHeaderConfigProps, + type StackHeaderTypeAndroid, + ScrollViewMarker, +} from 'react-native-screens'; + +const PLATFORM_COLOR_LIGHT = PlatformColor('@android:color/holo_green_light'); +const PLATFORM_COLOR_DARK = PlatformColor('@android:color/holo_green_dark'); + +const options = (...values: T[]): T[] => values; + +const HEADER_TYPES: StackHeaderTypeAndroid[] = ['small', 'medium', 'large']; +const COLOR_OPTIONS = options( + 'default', + 'red', + 'green', + 'blue', + 'translucent', + 'transparent', + 'platform', +); + +type ColorOption = (typeof COLOR_OPTIONS)[number]; + +interface Config { + type: StackHeaderTypeAndroid; + backgroundColor: ColorOption; + scrolledBackgroundColor: ColorOption; + backgroundSubview: boolean; +} + +const DEFAULT_CONFIG: Config = { + type: 'small', + backgroundColor: 'default', + scrolledBackgroundColor: 'default', + backgroundSubview: false, +}; + +function resolveBackgroundColor(value: ColorOption): ColorValue | undefined { + switch (value) { + case 'red': + return Colors.RedLight60; + case 'green': + return Colors.GreenLight60; + case 'blue': + return Colors.BlueLight60; + case 'translucent': + return Colors.NavyLightTransparent; + case 'transparent': + return 'transparent'; + case 'platform': + return PLATFORM_COLOR_LIGHT; + default: + return undefined; + } +} + +function resolveScrolledBackgroundColor( + value: ColorOption, +): ColorValue | undefined { + switch (value) { + case 'red': + return Colors.RedLight100; + case 'green': + return Colors.GreenLight100; + case 'blue': + return Colors.BlueLight100; + case 'translucent': + return Colors.NavyLightTransparent; + case 'transparent': + return 'transparent'; + case 'platform': + return PLATFORM_COLOR_DARK; + default: + return undefined; + } +} + +// The image fills only the trailing part of the header so the background color +// stays visible next to it (and behind it through the scrim when collapsed). +function TreesBackground() { + return ( + + + + ); +} + +function buildHeaderConfig(config: Config): StackHeaderConfigProps { + return { + title: 'Header background', + android: { + type: config.type, + + // Collapsing headers re-expand as soon as the user scrolls up, so the + // tester can compare expanded/collapsed appearance from any scroll offset. + scrollFlagEnterAlways: config.type === 'small' ? undefined : true, + + backgroundColor: resolveBackgroundColor(config.backgroundColor), + scrolledBackgroundColor: resolveScrolledBackgroundColor( + config.scrolledBackgroundColor, + ), + + // The background subview is supported only for collapsing header types. + backgroundSubview: + config.backgroundSubview && config.type !== 'small' + ? { collapseMode: 'parallax', render: () => } + : undefined, + }, + }; +} + +function TestStackHeaderBackgroundAndroid() { + return ( + + }]} + /> + + ); +} + +function ConfigScreen() { + const { setRouteOptions, routeKey } = useStackNavigationContext(); + const [config, setConfig] = useState(DEFAULT_CONFIG); + + const updateConfig = useCallback( + (key: K, value: Config[K]) => { + setConfig(prev => ({ ...prev, [key]: value })); + }, + [], + ); + + const headerConfig = useMemo(() => buildHeaderConfig(config), [config]); + + useEffect(() => { + setRouteOptions(routeKey, { headerConfig }); + }, [headerConfig, setRouteOptions, routeKey]); + + return ( + + + Header config + + testID="header-type-picker" + label="type" + value={config.type} + onValueChange={v => updateConfig('type', v)} + items={HEADER_TYPES} + /> + + testID="background-color-picker" + label="backgroundColor" + value={config.backgroundColor} + onValueChange={v => updateConfig('backgroundColor', v)} + items={COLOR_OPTIONS} + /> + + testID="scrolled-background-color-picker" + label="scrolledBackgroundColor" + value={config.scrolledBackgroundColor} + onValueChange={v => updateConfig('scrolledBackgroundColor', v)} + items={COLOR_OPTIONS} + /> + updateConfig('backgroundSubview', v)} + /> + + Scroll to observe color changes + + End of content + + + ); +} + +const styles = StyleSheet.create({ + // Shows through the header when header is transparent. + backdrop: { + flex: 1, + backgroundColor: Colors.PurpleLight80, + }, + scrollViewMarker: { + flex: 1, + }, + scroll: { + backgroundColor: Colors.cardBackground, + }, + content: { + padding: 16, + gap: 6, + }, + heading: { + fontSize: 20, + fontWeight: 'bold', + marginTop: 12, + marginBottom: 4, + }, + backgroundSubview: { + flex: 1, + alignItems: 'flex-end', + }, + backgroundImage: { + height: '100%', + width: '50%', + }, +}); + +export default createScenario( + TestStackHeaderBackgroundAndroid, + scenarioDescription, +); diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario-description.ts b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario-description.ts new file mode 100644 index 0000000000..3dd555442a --- /dev/null +++ b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario-description.ts @@ -0,0 +1,11 @@ +import type { ScenarioDescription } from '@apps/tests/shared/helpers'; + +export const scenarioDescription: ScenarioDescription = { + name: 'Header background (Android)', + key: 'test-stack-header-background-android', + details: + 'Verify header backgroundColor and scrolledBackgroundColor customization.', + platforms: ['android'], + e2eCoverage: 'incomplete', + smokeTest: false, +}; diff --git a/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario.md b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario.md new file mode 100644 index 0000000000..6b88cd2280 --- /dev/null +++ b/apps/src/tests/single-feature-tests/stack-v5/test-stack-header-background-android/scenario.md @@ -0,0 +1,127 @@ +# Test Scenario: Header background (Android) + +## Details + +**Description:** Exercises the Android Material 3 header background +customization: `backgroundColor` (the app bar background) and +`scrolledBackgroundColor` (the color shown when content is scrolled beneath +the header — the lift-on-scroll target color for the `small` header, the +`CollapsingToolbarLayout` content scrim for `medium`/`large`). Verifies that +custom colors keep the lift/scrim transitions working, that clearing a prop +restores the Material default, and that the colors compose with edge-to-edge +and the `backgroundSubview`. + +**OS test creation version:** API 37 + +## E2E test + +Incomplete: not automated. Every assertion in this scenario is about rendered +colors and animated transitions, neither of which Detox can read. + +## Prerequisites + +- Android emulator or device. + +## Note + +- The `backgroundSubview` switch only takes effect for `medium`/`large` + headers. The trees image fills only the trailing part of the header, so the + background color stays visible next to it. +- The whole stack is wrapped in a purple backdrop; it is visible through the + header when it is transparent. + +## Steps + +### Small header + +1. Navigate to **Stack v5 → Header background (Android)**. Leave defaults. + +- [ ] At rest the header uses the default surface color; scrolling up animates + it to the slightly darker default lifted color, scrolling back to top + returns it. + +2. Set `backgroundColor` = `red`. + +- [ ] The header (including the status bar area behind it) turns light red at + rest. +- [ ] Scrolling up animates light red to the default lifted color and back. + +3. Set `scrolledBackgroundColor` = `red`, then scroll. + +- [ ] The header darkens from light red to the stronger red when content + scrolls beneath it, and returns at the top. No flicker while scrolling. + +4. While scrolled (header in the stronger red), set `backgroundColor` = + `green`. + +- [ ] The header immediately keeps the scrolled (red) color without animating + from the resting color; scrolling to top reveals light green. + +5. Set `scrolledBackgroundColor` = `translucent`, then scroll. + +- [ ] The scrolled color blends over the background color (navy-tinted light + green) instead of replacing it. + +6. While scrolled, set `scrolledBackgroundColor` = `transparent`. + +- [ ] The header keeps the background color regardless of scroll position. + +7. Set both `backgroundColor` and `scrolledBackgroundColor` = `transparent`. + +- [ ] The header area (including the status bar area) shows the purple screen + backdrop through the fully transparent header; the title and buttons + remain visible. + +8. Set both pickers back to `default`. + +- [ ] The header returns to the Material default colors without a visible + rebuild. + +9. Set both `backgroundColor` and `scrolledBackgroundColor` = `platform`, + then scroll. + +- [ ] At rest the header uses the OS-resolved light green + (`holo_green_light`); scrolling darkens it to the OS-resolved dark green + (`holo_green_dark`) and back. + +### Medium / large header + +10. Set `type` = `medium` (later repeat with `large`), colors = `default`. + +- [ ] Expanded header shows the default surface color; collapsing it fades in + the default content scrim color over the toolbar area. + +11. Set `backgroundColor` = `green` and `scrolledBackgroundColor` = `green`. + +- [ ] Expanded header is light green, including the status bar area. +- [ ] Collapsing fades in the stronger green scrim; expanding fades it out + back to light green. + +12. Enable `backgroundSubview`. + +- [ ] The trees image fills the trailing part of the expanded header (also + behind the status bar) with the light green background visible next to + it, and moves with parallax while collapsing. +- [ ] The green scrim fades in above the image when collapsed; the title and + buttons stay above the scrim. + +13. Set `scrolledBackgroundColor` = `translucent` and collapse. + +- [ ] The scrim tints the image instead of fully covering it. + +14. Set `scrolledBackgroundColor` = `transparent` and collapse. + +- [ ] No scrim appears — the image and background color stay fully visible + when collapsed. + +15. Set both colors back to `default` with the subview still enabled. + +- [ ] Collapsed state shows the default scrim color again. + +### Type changes keep colors + +16. Set `backgroundColor` = `red`, `scrolledBackgroundColor` = `blue`, then + switch `type` across `small` → `medium` → `large` → `small`. + +- [ ] After every switch the header keeps light red at rest and blue when + scrolled/collapsed. From 3f7639297a69ab34311e1235afded90309349942 Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Wed, 19 Aug 2026 14:47:35 +0200 Subject: [PATCH 4/6] set background to MaterialShareDrawable directly instead of relying on hidden conversion --- .../rnscreens/stack/header/StackHeaderApplicator.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt index 8bb812a216..6cafdacb2b 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt @@ -16,7 +16,6 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.Toolbar import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.graphics.drawable.toDrawable import com.google.android.material.R import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS @@ -469,9 +468,14 @@ internal class StackHeaderApplicator( config.scrolledBackgroundColor ?: resolveColorAttr(appBar.context, R.attr.colorSurfaceContainer) + val background = + MaterialShapeDrawable().apply { + fillColor = ColorStateList.valueOf(backgroundColor) + } + when (appBar) { is StackHeaderAppBarLayout.Small -> { - appBar.background = backgroundColor.toDrawable() + appBar.background = background appBar.setLiftOnScrollColor(ColorStateList.valueOf(scrolledBackgroundColor)) // The lift animation runs only on lifted-state changes; jump to the end @@ -480,7 +484,7 @@ internal class StackHeaderApplicator( // initializeLiftOnScrollWithColor), not the raw scrolled color — they // differ when the scrolled color is not fully opaque. if (appBar.isLifted) { - (appBar.background as? MaterialShapeDrawable)?.fillColor = + background.fillColor = ColorStateList.valueOf( MaterialColors.layer(backgroundColor, scrolledBackgroundColor), ) @@ -489,7 +493,7 @@ internal class StackHeaderApplicator( is StackHeaderAppBarLayout.Collapsing -> { appBar.setLiftOnScrollColor(null) - appBar.background = backgroundColor.toDrawable() + appBar.background = background appBar.collapsingToolbarLayout.setContentScrimColor(scrolledBackgroundColor) } } From 4aa0a18fcfd7f298e9e0d517a8cf3c5e90e15007 Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Tue, 25 Aug 2026 14:30:38 +0200 Subject: [PATCH 5/6] add sources of default background colors --- .../rnscreens/stack/header/StackHeaderApplicator.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt index 6cafdacb2b..df19828489 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt @@ -462,8 +462,16 @@ internal class StackHeaderApplicator( appBar: StackHeaderAppBarLayout, config: StackHeaderConfigurationProviding, ) { + // Widget.Material3Expressive.AppBarLayout (an empty alias of Widget.Material3.AppBarLayout) + // #android:background = @macro/m3_comp_app_bar_container_color. val backgroundColor = config.backgroundColor ?: resolveColorAttr(appBar.context, R.attr.colorSurface) + + // Both header kinds default to the same token, from different places: + // small — ...AppBarLayout#liftOnScrollColor + // = @macro/m3_comp_app_bar_on_scroll_container_color; + // collapsing — the M3 CollapsingToolbar styles set no contentScrim, so CTL installs its + // own default for titleCollapseMode=fade (getDefaultContentScrimColorForTitleCollapseFadeMode). val scrolledBackgroundColor = config.scrolledBackgroundColor ?: resolveColorAttr(appBar.context, R.attr.colorSurfaceContainer) From a18920486b71b6c6f8ce623bbaf96454fe53a71a Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Tue, 25 Aug 2026 14:51:49 +0200 Subject: [PATCH 6/6] add comment explaining why we need to clear lift on scroll color in Collapsing header --- .../swmansion/rnscreens/stack/header/StackHeaderApplicator.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt index df19828489..a641634bc7 100644 --- a/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt +++ b/android/src/main/java/com/swmansion/rnscreens/stack/header/StackHeaderApplicator.kt @@ -500,6 +500,9 @@ internal class StackHeaderApplicator( } is StackHeaderAppBarLayout.Collapsing -> { + // Fade collapse mode disables liftOnScroll, but the lift animation only checks the + // lift color. We set it to null so the fill stays at backgroundColor under the + // transparent/translucent content scrim. appBar.setLiftOnScrollColor(null) appBar.background = background appBar.collapsingToolbarLayout.setContentScrimColor(scrolledBackgroundColor)