mirror of
https://github.com/flutter/flutter
synced 2024-11-05 18:37:51 +00:00
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:
parent
8e44e94e22
commit
6e246ac854
6 changed files with 542 additions and 62 deletions
|
@ -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>[
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue