diff --git a/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart b/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart index 97a211cc53..f879cede9e 100644 --- a/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart +++ b/examples/api/lib/cupertino/nav_bar/cupertino_navigation_bar.0.dart @@ -35,6 +35,7 @@ class _NavBarExampleState extends State { // Try removing opacity to observe the lack of a blur effect and of sliding content. backgroundColor: CupertinoColors.systemGrey.withOpacity(0.5), middle: const Text('CupertinoNavigationBar Sample'), + automaticBackgroundVisibility: false, ), child: Column( children: [ diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index 524f650d51..495fd62e15 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -34,6 +34,12 @@ const double _kNavBarLargeTitleHeightExtension = 52.0; /// from the normal navigation bar to a big title below the navigation bar. const double _kNavBarShowLargeTitleThreshold = 10.0; +/// Number of logical pixels scrolled during which the navigation bar's background +/// fades in or out. +/// +/// Eyeballed on the native Settings app on an iPhone 15 simulator running iOS 17.4. +const double _kNavBarScrollUnderAnimationExtent = 10.0; + const double _kNavBarEdgePadding = 16.0; const double _kNavBarBottomPadding = 8.0; @@ -52,6 +58,8 @@ const Border _kDefaultNavBarBorder = Border( ), ); +const Border _kTransparentNavBarBorder = Border(bottom: BorderSide(color: Color(0x00000000), width: 0.0)); + // There's a single tag for all instances of navigation bars because they can // all transition between each other (per Navigator) via Hero transitions. const _HeroTag _defaultHeroTag = _HeroTag(null); @@ -172,12 +180,9 @@ Widget _wrapWithBackground({ child: result, ); - if (backgroundColor.alpha == 0xFF) { - return childWithBackground; - } - return ClipRect( child: BackdropFilter( + enabled: backgroundColor.alpha != 0xFF, filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), child: childWithBackground, ), @@ -260,6 +265,7 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer this.trailing, this.border = _kDefaultNavBarBorder, this.backgroundColor, + this.automaticBackgroundVisibility = true, this.brightness, this.padding, this.transitionBetweenRoutes = true, @@ -338,10 +344,26 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer /// tab bar will automatically produce a blurring effect to the content /// behind it. /// + /// By default, the navigation bar's background is visible only when scrolled under. + /// This behavior can be controlled with [automaticBackgroundVisibility]. + /// /// Defaults to [CupertinoTheme]'s `barBackgroundColor` if null. /// {@endtemplate} final Color? backgroundColor; + /// {@template flutter.cupertino.CupertinoNavigationBar.automaticBackgroundVisibility} + /// Whether the navigation bar appears transparent when no content is scrolled under. + /// + /// If this is true, the navigation bar's background color will be transparent + /// until the content scrolls under it. If false, the navigation bar will always + /// use [backgroundColor] as its background color. + /// + /// If the navigation bar is not a child of a [CupertinoPageScaffold], this has no effect. + /// + /// This value defaults to true. + /// {@endtemplate} + final bool automaticBackgroundVisibility; + /// {@template flutter.cupertino.CupertinoNavigationBar.brightness} /// The brightness of the specified [backgroundColor]. /// @@ -435,17 +457,83 @@ class CupertinoNavigationBar extends StatefulWidget implements ObstructingPrefer class _CupertinoNavigationBarState extends State { late _NavigationBarStaticComponentsKeys keys; + ScrollNotificationObserverState? _scrollNotificationObserver; + double _scrollAnimationValue = 0.0; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _scrollNotificationObserver?.removeListener(_handleScrollNotification); + _scrollNotificationObserver = ScrollNotificationObserver.maybeOf(context); + _scrollNotificationObserver?.addListener(_handleScrollNotification); + } + + @override + void dispose() { + if (_scrollNotificationObserver != null) { + _scrollNotificationObserver!.removeListener(_handleScrollNotification); + _scrollNotificationObserver = null; + } + super.dispose(); + } + @override void initState() { super.initState(); keys = _NavigationBarStaticComponentsKeys(); } + void _handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollUpdateNotification && notification.depth == 0) { + final ScrollMetrics metrics = notification.metrics; + final double oldScrollAnimationValue = _scrollAnimationValue; + double scrollExtent = 0.0; + switch (metrics.axisDirection) { + case AxisDirection.up: + // Scroll view is reversed + scrollExtent = metrics.extentAfter; + case AxisDirection.down: + scrollExtent = metrics.extentBefore; + case AxisDirection.right: + case AxisDirection.left: + // Scrolled under is only supported in the vertical axis, and should + // not be altered based on horizontal notifications of the same + // predicate since it could be a 2D scroller. + break; + } + + if (scrollExtent >= 0 && scrollExtent < _kNavBarScrollUnderAnimationExtent) { + setState(() { + _scrollAnimationValue = clampDouble(scrollExtent / _kNavBarScrollUnderAnimationExtent, 0, 1); + }); + } else if (scrollExtent > _kNavBarScrollUnderAnimationExtent && oldScrollAnimationValue != 1.0) { + setState(() { + _scrollAnimationValue = 1.0; + }); + } else if (scrollExtent <= 0 && oldScrollAnimationValue != 0.0) { + setState(() { + _scrollAnimationValue = 0.0; + }); + } + } + } + @override Widget build(BuildContext context) { final Color backgroundColor = CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor; + final Color? parentPageScaffoldBackgroundColor = CupertinoPageScaffoldBackgroundColor.maybeOf(context); + + final Border? initialBorder = widget.automaticBackgroundVisibility && parentPageScaffoldBackgroundColor != null + ? _kTransparentNavBarBorder + : widget.border; + final Border? effectiveBorder = widget.border == null ? null : Border.lerp(initialBorder, widget.border, _scrollAnimationValue,); + + final Color effectiveBackgroundColor = widget.automaticBackgroundVisibility && parentPageScaffoldBackgroundColor != null + ? Color.lerp(parentPageScaffoldBackgroundColor, backgroundColor, _scrollAnimationValue) ?? backgroundColor + : backgroundColor; + final _NavigationBarStaticComponents components = _NavigationBarStaticComponents( keys: keys, route: ModalRoute.of(context), @@ -461,8 +549,8 @@ class _CupertinoNavigationBarState extends State { ); final Widget navBar = _wrapWithBackground( - border: widget.border, - backgroundColor: backgroundColor, + border: effectiveBorder, + backgroundColor: effectiveBackgroundColor, brightness: widget.brightness, child: DefaultTextStyle( style: CupertinoTheme.of(context).textTheme.textStyle, @@ -491,11 +579,11 @@ class _CupertinoNavigationBarState extends State { transitionOnUserGestures: true, child: _TransitionableNavigationBar( componentsKeys: keys, - backgroundColor: backgroundColor, + backgroundColor: effectiveBackgroundColor, backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle, titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle, largeTitleTextStyle: null, - border: widget.border, + border: effectiveBorder, hasUserMiddle: widget.middle != null, largeExpanded: false, child: navBar, @@ -587,6 +675,7 @@ class CupertinoSliverNavigationBar extends StatefulWidget { this.trailing, this.border = _kDefaultNavBarBorder, this.backgroundColor, + this.automaticBackgroundVisibility = true, this.brightness, this.padding, this.transitionBetweenRoutes = true, @@ -668,6 +757,9 @@ class CupertinoSliverNavigationBar extends StatefulWidget { /// {@macro flutter.cupertino.CupertinoNavigationBar.backgroundColor} final Color? backgroundColor; + /// {@macro flutter.cupertino.CupertinoNavigationBar.automaticBackgroundVisibility} + final bool automaticBackgroundVisibility; + /// {@macro flutter.cupertino.CupertinoNavigationBar.brightness} final Brightness? brightness; @@ -738,6 +830,7 @@ class _CupertinoSliverNavigationBarState extends State { Widget build(BuildContext context) { Widget paddedContent = widget.child; + final Color backgroundColor = CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) + ?? CupertinoTheme.of(context).scaffoldBackgroundColor; + final MediaQueryData existingMediaQuery = MediaQuery.of(context); if (widget.navigationBar != null) { // TODO(xster): Use real size after partial layout instead of preferred size. @@ -171,42 +175,79 @@ class _CupertinoPageScaffoldState extends State { ); } - return DecoratedBox( - decoration: BoxDecoration( - color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) - ?? CupertinoTheme.of(context).scaffoldBackgroundColor, - ), - child: Stack( - children: [ - // The main content being at the bottom is added to the stack first. - paddedContent, - if (widget.navigationBar != null) - Positioned( - top: 0.0, - left: 0.0, - right: 0.0, - child: MediaQuery.withNoTextScaling( - child: widget.navigationBar!, + return ScrollNotificationObserver( + child: DecoratedBox( + decoration: BoxDecoration( + color: backgroundColor, + ), + child: CupertinoPageScaffoldBackgroundColor( + color: backgroundColor, + child: Stack( + children: [ + // The main content being at the bottom is added to the stack first. + paddedContent, + if (widget.navigationBar != null) + Positioned( + top: 0.0, + left: 0.0, + right: 0.0, + child: MediaQuery.withNoTextScaling( + child: widget.navigationBar!, + ), + ), + // Add a touch handler the size of the status bar on top of all contents + // to handle scroll to top by status bar taps. + Positioned( + top: 0.0, + left: 0.0, + right: 0.0, + height: existingMediaQuery.padding.top, + child: GestureDetector( + excludeFromSemantics: true, + onTap: _handleStatusBarTap, + ), ), - ), - // Add a touch handler the size of the status bar on top of all contents - // to handle scroll to top by status bar taps. - Positioned( - top: 0.0, - left: 0.0, - right: 0.0, - height: existingMediaQuery.padding.top, - child: GestureDetector( - excludeFromSemantics: true, - onTap: _handleStatusBarTap, - ), + ], ), - ], + ), ), ); } } +/// [InheritedWidget] indicating what the current scaffold background color is for its children. +/// +/// This is used by the [CupertinoNavigationBar] and the [CupertinoSliverNavigationBar] widgets +/// to paint themselves with the parent page scaffold color when no content is scrolled under. +class CupertinoPageScaffoldBackgroundColor extends InheritedWidget { + /// Constructs a new [CupertinoPageScaffoldBackgroundColor]. + const CupertinoPageScaffoldBackgroundColor({ + required super.child, + required this.color, + super.key, + }); + + /// The background color defined in [CupertinoPageScaffold]. + final Color color; + + @override + bool updateShouldNotify(CupertinoPageScaffoldBackgroundColor oldWidget) { + return color != oldWidget.color; + } + + /// Retrieve the [CupertinoPageScaffold] background color from the context. + static Color? maybeOf(BuildContext context) { + final CupertinoPageScaffoldBackgroundColor? scaffoldBackgroundColor = context.dependOnInheritedWidgetOfExactType(); + return scaffoldBackgroundColor?.color; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('page scaffold background color', color)); + } +} + /// Widget that has a preferred size and reports whether it fully obstructs /// widgets behind it. /// diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 49670a3aa6..38830fc479 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -1202,14 +1202,31 @@ class RenderBackdropFilter extends RenderProxyBox { /// Creates a backdrop filter. // /// The [blendMode] argument defaults to [BlendMode.srcOver]. - RenderBackdropFilter({ RenderBox? child, required ui.ImageFilter filter, BlendMode blendMode = BlendMode.srcOver }) + RenderBackdropFilter({ + RenderBox? child, + required ui.ImageFilter filter, + BlendMode blendMode = BlendMode.srcOver, + bool enabled = true, + }) : _filter = filter, + _enabled = enabled, _blendMode = blendMode, super(child); @override BackdropFilterLayer? get layer => super.layer as BackdropFilterLayer?; + /// Whether or not the backdrop filter operation will be applied to the child. + bool get enabled => _enabled; + bool _enabled; + set enabled(bool value) { + if (enabled == value) { + return; + } + _enabled = value; + markNeedsPaint(); + } + /// The image filter to apply to the existing painted content before painting /// the child. /// @@ -1244,6 +1261,11 @@ class RenderBackdropFilter extends RenderProxyBox { @override void paint(PaintingContext context, Offset offset) { + if (!_enabled) { + super.paint(context, offset); + return; + } + if (child != null) { assert(needsCompositing); layer ??= BackdropFilterLayer(); diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 766ce54e05..df63e705fe 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -559,6 +559,7 @@ class BackdropFilter extends SingleChildRenderObjectWidget { required this.filter, super.child, this.blendMode = BlendMode.srcOver, + this.enabled = true, }); /// The image filter to apply to the existing painted content before painting the child. @@ -573,15 +574,23 @@ class BackdropFilter extends SingleChildRenderObjectWidget { /// {@macro flutter.widgets.BackdropFilter.blendMode} final BlendMode blendMode; + /// Whether or not to apply the backdrop filter operation to the child of this + /// widget. + /// + /// Prefer setting enabled to `false` instead of creating a "no-op" filter + /// type for performance reasons. + final bool enabled; + @override RenderBackdropFilter createRenderObject(BuildContext context) { - return RenderBackdropFilter(filter: filter, blendMode: blendMode); + return RenderBackdropFilter(filter: filter, blendMode: blendMode, enabled: enabled); } @override void updateRenderObject(BuildContext context, RenderBackdropFilter renderObject) { renderObject ..filter = filter + ..enabled = enabled ..blendMode = blendMode; } } diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index d757445c55..faf500e9d6 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -64,40 +64,97 @@ void main() { darkColor: Color(0xF3E5E5E5), ); + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); + await tester.pumpWidget( - const CupertinoApp( - theme: CupertinoThemeData(brightness: Brightness.light), - home: CupertinoNavigationBar( - middle: Text('Title'), - backgroundColor: background, + CupertinoApp( + theme: const CupertinoThemeData(brightness: Brightness.light), + home: CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Title'), + backgroundColor: background, + ), + child: ListView( + controller: scrollController, + children: const [ + Placeholder(), + ], + ), ), ), ); - expect(find.byType(BackdropFilter), findsNothing); + + scrollController.jumpTo(100.0); + await tester.pump(); + + expect( + tester.widget(find.byType(BackdropFilter)), + isA().having((BackdropFilter filter) => filter.enabled, 'filter enabled', false), + ); expect(find.byType(CupertinoNavigationBar), paints..rect(color: background.color)); await tester.pumpWidget( - const CupertinoApp( - theme: CupertinoThemeData(brightness: Brightness.dark), - home: CupertinoNavigationBar( - middle: Text('Title'), - backgroundColor: background, + CupertinoApp( + theme: const CupertinoThemeData(brightness: Brightness.dark), + home: CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Title'), + backgroundColor: background, + ), + child: ListView( + controller: scrollController, + children: const [ + Placeholder(), + ], + ), ), ), ); - expect(find.byType(BackdropFilter), findsOneWidget); + + scrollController.jumpTo(100.0); + await tester.pump(); + + expect( + tester.widget(find.byType(BackdropFilter)), + isA().having((BackdropFilter f) => f.enabled, 'filter enabled', true), + ); expect(find.byType(CupertinoNavigationBar), paints..rect(color: background.darkColor)); }); - testWidgets('Non-opaque background adds blur effects', (WidgetTester tester) async { - await tester.pumpWidget( - const CupertinoApp( - home: CupertinoNavigationBar( - middle: Text('Title'), + testWidgets("Background doesn't add blur effect when no content is scrolled under", (WidgetTester test) async { + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); + + await test.pumpWidget( + CupertinoApp( + theme: const CupertinoThemeData(brightness: Brightness.light), + home: CupertinoPageScaffold( + navigationBar: const CupertinoNavigationBar( + middle: Text('Title'), + ), + child: ListView( + controller: scrollController, + children: const [ + Placeholder(), + ], + ), ), ), ); - expect(find.byType(BackdropFilter), findsOneWidget); + + expect( + test.widget(find.byType(BackdropFilter)), + isA().having((BackdropFilter filter) => filter.enabled, 'filter enabled', false), + ); + + scrollController.jumpTo(100.0); + await test.pump(); + + expect( + test.widget(find.byType(BackdropFilter)), + isA().having((BackdropFilter filter) => filter.enabled, 'filter enabled', true), + ); }); testWidgets('Nav bar displays correctly', (WidgetTester tester) async { @@ -781,6 +838,7 @@ void main() { await tester.pumpWidget( const CupertinoApp( home: CupertinoNavigationBar( + automaticBackgroundVisibility: false, middle: Text('Title'), border: Border( bottom: BorderSide( @@ -933,6 +991,7 @@ void main() { child: CustomScrollView( slivers: [ CupertinoSliverNavigationBar( + automaticBackgroundVisibility: false, largeTitle: Text('Large Title'), border: Border( bottom: BorderSide( @@ -1018,6 +1077,241 @@ void main() { }, ); + testWidgets( + 'Nav bar background is transparent if `automaticBackgroundVisibility` is true and has no content scrolled under it', + (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); + + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + backgroundColor: const Color(0xFFFFFFFF), + navigationBar: const CupertinoNavigationBar( + backgroundColor: Color(0xFFE5E5E5), + border: Border( + bottom: BorderSide( + color: Color(0xFFAABBCC), + width: 0.0, + ), + ), + middle: Text('Title'), + ), + child: ListView( + controller: scrollController, + children: const [ + Placeholder(), + ], + ), + ), + ), + ); + + expect(scrollController.offset, 0.0); + + final DecoratedBox decoratedBox = tester.widgetList(find.descendant( + of: find.byType(CupertinoNavigationBar), + matching: find.byType(DecoratedBox), + )).first as DecoratedBox; + expect(decoratedBox.decoration.runtimeType, BoxDecoration); + + final BoxDecoration decoration = decoratedBox.decoration as BoxDecoration; + final BorderSide side = decoration.border!.bottom; + expect(side.color.opacity, 0.0); + + // Appears transparent since the background color is the same as the scaffold. + expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFFFFFFF))); + + scrollController.jumpTo(100.0); + await tester.pump(); + + final DecoratedBox decoratedBoxAfterSroll = tester.widgetList(find.descendant( + of: find.byType(CupertinoNavigationBar), + matching: find.byType(DecoratedBox), + )).first as DecoratedBox; + expect(decoratedBoxAfterSroll.decoration.runtimeType, BoxDecoration); + + final BorderSide borderAfterScroll = (decoratedBoxAfterSroll.decoration as BoxDecoration).border!.bottom; + + expect(borderAfterScroll.color.opacity, 1.0); + + expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFE5E5E5))); + }, + ); + + testWidgets( + 'automaticBackgroundVisibility parameter has no effect if nav bar is not a child of CupertinoPageScaffold', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: CupertinoNavigationBar( + backgroundColor: Color(0xFFE5E5E5), + border: Border( + bottom: BorderSide( + color: Color(0xFFAABBCC), + width: 0.0, + ), + ), + middle: Text('Title'), + ), + ), + ); + + final DecoratedBox decoratedBox = tester.widgetList(find.descendant( + of: find.byType(CupertinoNavigationBar), + matching: find.byType(DecoratedBox), + )).first as DecoratedBox; + expect(decoratedBox.decoration.runtimeType, BoxDecoration); + + final BoxDecoration decoration = decoratedBox.decoration as BoxDecoration; + final BorderSide side = decoration.border!.bottom; + expect(side.color, const Color(0xFFAABBCC)); + + expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFE5E5E5))); + }, + ); + + testWidgets( + 'Nav bar background is always visible if `automaticBackgroundVisibility` is false', + (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Color(0xFFE5E5E5), + border: Border( + bottom: BorderSide( + color: Color(0xFFAABBCC), + width: 0.0, + ), + ), + middle: Text('Title'), + ), + child: Placeholder(), + ), + ), + ); + + DecoratedBox decoratedBox = tester.widgetList(find.descendant( + of: find.byType(CupertinoNavigationBar), + matching: find.byType(DecoratedBox), + )).first as DecoratedBox; + expect(decoratedBox.decoration.runtimeType, BoxDecoration); + + BoxDecoration decoration = decoratedBox.decoration as BoxDecoration; + BorderSide side = decoration.border!.bottom; + expect(side.color, const Color(0xFFAABBCC)); + + expect(find.byType(CupertinoNavigationBar), paints..rect(color: const Color(0xFFE5E5E5))); + + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + child: CustomScrollView( + slivers: [ + const CupertinoSliverNavigationBar( + automaticBackgroundVisibility: false, + backgroundColor: Color(0xFFE5E5E5), + border: Border( + bottom: BorderSide( + color: Color(0xFFAABBCC), + width: 0.0, + ), + ), + largeTitle: Text('Title'), + ), + SliverToBoxAdapter( + child: Container( + height: 1200.0, + ), + ), + ], + ), + ), + ), + ); + + decoratedBox = tester.widgetList(find.descendant( + of: find.byType(CupertinoSliverNavigationBar), + matching: find.byType(DecoratedBox), + )).first as DecoratedBox; + expect(decoratedBox.decoration.runtimeType, BoxDecoration); + + decoration = decoratedBox.decoration as BoxDecoration; + side = decoration.border!.bottom; + expect(side.color, const Color(0xFFAABBCC)); + + expect(find.byType(CupertinoSliverNavigationBar), paints..rect(color: const Color(0xFFE5E5E5))); + }, + ); + + testWidgets( + 'CupertinoSliverNavigationBar background is transparent if `automaticBackgroundVisibility` is true and has no content scrolled under it', + (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + addTearDown(scrollController.dispose); + + await tester.pumpWidget( + CupertinoApp( + home: CupertinoPageScaffold( + backgroundColor: const Color(0xFFFFFFFF), + child: CustomScrollView( + controller: scrollController, + slivers: [ + const CupertinoSliverNavigationBar( + backgroundColor: Color(0xFFE5E5E5), + border: Border( + bottom: BorderSide( + color: Color(0xFFAABBCC), + width: 0.0, + ), + ), + largeTitle: Text('Title'), + ), + SliverToBoxAdapter( + child: Container( + height: 1200.0, + ), + ), + ], + ), + ), + ), + ); + + expect(scrollController.offset, 0.0); + + final DecoratedBox decoratedBox = tester.widgetList(find.descendant( + of: find.byType(CupertinoSliverNavigationBar), + matching: find.byType(DecoratedBox), + )).first as DecoratedBox; + expect(decoratedBox.decoration.runtimeType, BoxDecoration); + + final BoxDecoration decoration = decoratedBox.decoration as BoxDecoration; + final BorderSide side = decoration.border!.bottom; + expect(side.color.opacity, 0.0); + + // Appears transparent since the background color is the same as the scaffold. + expect(find.byType(CupertinoSliverNavigationBar), paints..rect(color: const Color(0xFFFFFFFF))); + + scrollController.jumpTo(400.0); + await tester.pump(); + + final DecoratedBox decoratedBoxAfterSroll = tester.widgetList(find.descendant( + of: find.byType(CupertinoSliverNavigationBar), + matching: find.byType(DecoratedBox), + )).first as DecoratedBox; + expect(decoratedBoxAfterSroll.decoration.runtimeType, BoxDecoration); + + final BorderSide borderAfterScroll = (decoratedBoxAfterSroll.decoration as BoxDecoration).border!.bottom; + + expect(borderAfterScroll.color.opacity, 1.0); + + expect(find.byType(CupertinoSliverNavigationBar), paints..rect(color: const Color(0xFFE5E5E5))); + }, + ); + testWidgets('NavBar draws a light system bar for a dark background', (WidgetTester tester) async { await tester.pumpWidget( WidgetsApp(