Cupertino transparent navigation bars (#149102)

This PR is making the `CupertinoNavigationBar` and `CupertinoSliverNavigationBar` appear transparent as long as the content is not scrolled under them, so they look like standard iOS apps nav bars.

https://github.com/flutter/flutter/assets/423393/eee2700b-2a91-4577-922c-6163d47cb357

https://github.com/flutter/flutter/assets/423393/3847f2b5-0dac-4d5e-aa6f-03c1d2893e30

<details>
  <summary>Demo app code</summary>
  
  ```dart
import 'package:flutter/cupertino.dart';

/// Flutter code sample for [CupertinoTabScaffold].

void main() => runApp(const TabScaffoldApp());

class TabScaffoldApp extends StatefulWidget {
  const TabScaffoldApp({super.key});

  @override
  State<TabScaffoldApp> createState() => _TabScaffoldAppState();
}

class _TabScaffoldAppState extends State<TabScaffoldApp> {
  Brightness _brightness = Brightness.light;

  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      theme: CupertinoThemeData(brightness: _brightness),
      home: TabScaffoldExample(
          brightness: _brightness, onBrightnessToggle: _toggleBrightness),
    );
  }

  void _toggleBrightness() {
    setState(() {
      _brightness =
          _brightness == Brightness.light ? Brightness.dark : Brightness.light;
    });
  }
}

class TabScaffoldExample extends StatefulWidget {
  const TabScaffoldExample(
      {required this.brightness, required this.onBrightnessToggle, super.key});

  final VoidCallback onBrightnessToggle;
  final Brightness brightness;

  @override
  State<TabScaffoldExample> createState() => _TabScaffoldExampleState();
}

class _TabScaffoldExampleState extends State<TabScaffoldExample> {
  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search_circle_fill),
            label: 'Explore',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.person_fill),
            label: 'Profile',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.settings_solid),
            label: 'Settings',
          ),
        ],
      ),
      tabBuilder: (BuildContext context, int index) {
        return CupertinoTabView(
          builder: (BuildContext context) {
            return CupertinoPageScaffold(
              backgroundColor: index == 3
                  ? CupertinoColors.secondarySystemBackground
                      .resolveFrom(context)
                  : null,
              child: CustomScrollView(
                slivers: [
                  CupertinoSliverNavigationBar(
                    largeTitle: Text('Tab $index'),
                    initiallyTransparent: index != 2,
                    trailing: CupertinoButton(
                      padding: EdgeInsets.zero,
                      onPressed: widget.onBrightnessToggle,
                      child: Icon(
                        widget.brightness == Brightness.light
                            ? CupertinoIcons.moon_stars
                            : CupertinoIcons.sun_max,
                      ),
                    ),
                  ),
                  SliverSafeArea(
                    top: false,
                    sliver: SliverList.list(
                      children: [
                        CupertinoButton(
                          child: const Text('Next page'),
                          onPressed: () {
                            Navigator.of(context).push(
                              CupertinoPageRoute<void>(
                                builder: (BuildContext context) {
                                  return CupertinoPageScaffold(
                                    navigationBar: CupertinoNavigationBar(
                                      middle: Text('Inner page of tab $index'),
                                    ),
                                    child: ListView(
                                      children: [
                                        Center(
                                          child: CupertinoButton(
                                            child: const Text('Back'),
                                            onPressed: () {
                                              Navigator.of(context).pop();
                                            },
                                          ),
                                        ),
                                        if (index == 0) const _LongList(),
                                        const SizedBox(height: 20),
                                      ],
                                    ),
                                  );
                                },
                              ),
                            );
                          },
                        ),
                        if (index == 1) const _LongList(),
                        const SizedBox(height: 20),
                      ],
                    ),
                  ),
                ],
              ),
            );
          },
        );
      },
    );
  }
}

class _LongList extends StatelessWidget {
  const _LongList();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        for (int i = 0; i < 50; i++) ...[
          CupertinoListTile(
            leading: const Icon(CupertinoIcons.book),
            title: Text('Bookstore item $i'),
          ),
        ],
      ],
    );
  }
}
  ```
</details>

This is the continuation of https://github.com/flutter/flutter/pull/142439.

I tried to keep the simplest API possible, so it's only introducing a new `automaticBackgroundVisibility` boolean parameter. 

In the implementation I had to use the `CupertinoPageScaffold` background color to make it look transparent instead of a 0 opacity, because of issues with route transitions.

I used an `InheritedWidget` so the nav bar is always getting the right background color from the parent scaffold, whether it is overridden or not. It would probably be better to make the inherited widget private but we'd need to move all the nav bar code to the same file as the scaffold, so for now I've just hidden it from the export. Let me know if it is okay to do that.

This PR is not dealing with the bottom tab bar, because the same [straightforward approach](dde8ec6dc7) doesn't work here. The problem is that the scroll notification is sent only when the scroll view is created or updated, so it doesn't work if one pushes a screen and navigates back.

Issues:
- #78607 
- #60411
This commit is contained in:
Vincent Velociter 2024-07-03 22:35:27 +02:00 committed by GitHub
parent 8e44e94e22
commit 6e246ac854
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 542 additions and 62 deletions

View file

@ -35,6 +35,7 @@ class _NavBarExampleState extends State<NavBarExample> {
// 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: <Widget>[

View file

@ -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<CupertinoNavigationBar> {
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<CupertinoNavigationBar> {
);
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<CupertinoNavigationBar> {
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<CupertinoSliverNavigation
components: components,
userMiddle: widget.middle,
backgroundColor: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? CupertinoTheme.of(context).barBackgroundColor,
automaticBackgroundVisibility: widget.automaticBackgroundVisibility,
brightness: widget.brightness,
border: widget.border,
padding: widget.padding,
@ -760,6 +853,7 @@ class _LargeTitleNavigationBarSliverDelegate
required this.components,
required this.userMiddle,
required this.backgroundColor,
required this.automaticBackgroundVisibility,
required this.brightness,
required this.border,
required this.padding,
@ -775,6 +869,7 @@ class _LargeTitleNavigationBarSliverDelegate
final _NavigationBarStaticComponents components;
final Widget? userMiddle;
final Color backgroundColor;
final bool automaticBackgroundVisibility;
final Brightness? brightness;
final Border? border;
final EdgeInsetsDirectional? padding;
@ -795,7 +890,13 @@ class _LargeTitleNavigationBarSliverDelegate
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
final double largeTitleThreshold = maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
final bool showLargeTitle = shrinkOffset < largeTitleThreshold;
final double shrinkAnimationValue = clampDouble(
(shrinkOffset - largeTitleThreshold - _kNavBarScrollUnderAnimationExtent) / _kNavBarScrollUnderAnimationExtent,
0,
1,
);
final _PersistentNavigationBar persistentNavigationBar =
_PersistentNavigationBar(
@ -806,9 +907,20 @@ class _LargeTitleNavigationBarSliverDelegate
middleVisible: alwaysShowMiddle ? null : !showLargeTitle,
);
final Color? parentPageScaffoldBackgroundColor = CupertinoPageScaffoldBackgroundColor.maybeOf(context);
final Border? initialBorder = automaticBackgroundVisibility && parentPageScaffoldBackgroundColor != null
? _kTransparentNavBarBorder
: border;
final Border? effectiveBorder = border == null ? null : Border.lerp(initialBorder, border, shrinkAnimationValue);
final Color effectiveBackgroundColor = automaticBackgroundVisibility && parentPageScaffoldBackgroundColor != null
? Color.lerp(parentPageScaffoldBackgroundColor, backgroundColor, shrinkAnimationValue) ?? backgroundColor
: backgroundColor;
final Widget navBar = _wrapWithBackground(
border: border,
backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context),
border: effectiveBorder,
backgroundColor: effectiveBackgroundColor,
brightness: brightness,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
@ -878,11 +990,11 @@ class _LargeTitleNavigationBarSliverDelegate
// needs to wrap the top level RenderBox rather than a RenderSliver.
child: _TransitionableNavigationBar(
componentsKeys: keys,
backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context),
backgroundColor: effectiveBackgroundColor,
backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle,
titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
border: border,
border: effectiveBorder,
hasUserMiddle: userMiddle != null && (alwaysShowMiddle || !showLargeTitle),
largeExpanded: showLargeTitle,
child: navBar,
@ -895,6 +1007,7 @@ class _LargeTitleNavigationBarSliverDelegate
return components != oldDelegate.components
|| userMiddle != oldDelegate.userMiddle
|| backgroundColor != oldDelegate.backgroundColor
|| automaticBackgroundVisibility != oldDelegate.automaticBackgroundVisibility
|| border != oldDelegate.border
|| padding != oldDelegate.padding
|| actionsForegroundColor != oldDelegate.actionsForegroundColor

View file

@ -8,6 +8,7 @@
/// @docImport 'route.dart';
library;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
@ -107,6 +108,9 @@ class _CupertinoPageScaffoldState extends State<CupertinoPageScaffold> {
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<CupertinoPageScaffold> {
);
}
return DecoratedBox(
decoration: BoxDecoration(
color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context)
?? CupertinoTheme.of(context).scaffoldBackgroundColor,
),
child: Stack(
children: <Widget>[
// 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: <Widget>[
// 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<CupertinoPageScaffoldBackgroundColor>();
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.
///

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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 <Widget>[
Placeholder(),
],
),
),
),
);
expect(find.byType(BackdropFilter), findsNothing);
scrollController.jumpTo(100.0);
await tester.pump();
expect(
tester.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().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 <Widget>[
Placeholder(),
],
),
),
),
);
expect(find.byType(BackdropFilter), findsOneWidget);
scrollController.jumpTo(100.0);
await tester.pump();
expect(
tester.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().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 <Widget>[
Placeholder(),
],
),
),
),
);
expect(find.byType(BackdropFilter), findsOneWidget);
expect(
test.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().having((BackdropFilter filter) => filter.enabled, 'filter enabled', false),
);
scrollController.jumpTo(100.0);
await test.pump();
expect(
test.widget(find.byType(BackdropFilter)),
isA<BackdropFilter>().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: <Widget>[
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 <Widget>[
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: <Widget>[
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: <Widget>[
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(