Unverified Commit 55dc9f93 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Refactor `SliverAppBar.medium` & `SliverAppBar.large` to fix several issues (#122542)

Refactor `SliverAppBar.medium` & `SliverAppBar.large` to fix several issues
parent 62cb61d3
......@@ -34,6 +34,8 @@ import 'theme.dart';
const double _kLeadingWidth = kToolbarHeight; // So the leading button is square.
const double _kMaxTitleTextScaleFactor = 1.34; // TODO(perc): Add link to Material spec when available, https://github.com/flutter/flutter/issues/58769.
enum _SliverAppVariant { small, medium, large }
// Bottom justify the toolbarHeight child which may overflow the top.
class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
const _ToolbarContainerLayout(this.toolbarHeight);
......@@ -1191,7 +1193,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
required this.titleTextStyle,
required this.systemOverlayStyle,
required this.forceMaterialTransparency,
required this.clipBehavior
required this.clipBehavior,
required this.variant,
}) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize.height ?? 0.0;
......@@ -1228,6 +1231,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double _bottomHeight;
final bool forceMaterialTransparency;
final Clip? clipBehavior;
final _SliverAppVariant variant;
@override
double get minExtent => collapsedHeight;
......@@ -1258,6 +1262,17 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double toolbarOpacity = !pinned || isPinnedWithOpacityFade
? clampDouble(visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight), 0.0, 1.0)
: 1.0;
final Widget? effectiveTitle;
if (variant == _SliverAppVariant.small) {
effectiveTitle = title;
} else {
effectiveTitle = AnimatedOpacity(
opacity: isScrolledUnder ? 1 : 0,
duration: const Duration(milliseconds: 500),
curve: const Cubic(0.2, 0.0, 0.0, 1.0),
child: title,
);
}
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
......@@ -1269,7 +1284,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
clipBehavior: clipBehavior,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
title: title,
title: effectiveTitle,
actions: actions,
flexibleSpace: (title == null && flexibleSpace != null && !excludeHeaderSemantics)
? Semantics(
......@@ -1474,7 +1489,11 @@ class SliverAppBar extends StatefulWidget {
this.clipBehavior,
}) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0),
assert(collapsedHeight == null || collapsedHeight >= toolbarHeight, 'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].');
assert(
collapsedHeight == null || collapsedHeight >= toolbarHeight,
'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].',
),
_variant = _SliverAppVariant.small;
/// Creates a Material Design medium top app bar that can be placed
/// in a [CustomScrollView].
......@@ -1499,87 +1518,50 @@ class SliverAppBar extends StatefulWidget {
/// * [SliverAppBar.large], for a large top app bar.
/// * https://m3.material.io/components/top-app-bar/overview, the Material 3
/// app bar specification.
factory SliverAppBar.medium({
Key? key,
Widget? leading,
bool automaticallyImplyLeading = true,
Widget? title,
List<Widget>? actions,
Widget? flexibleSpace,
PreferredSizeWidget? bottom,
double? elevation,
double? scrolledUnderElevation,
Color? shadowColor,
Color? surfaceTintColor,
bool forceElevated = false,
Color? backgroundColor,
Color? foregroundColor,
IconThemeData? iconTheme,
IconThemeData? actionsIconTheme,
bool primary = true,
bool? centerTitle,
bool excludeHeaderSemantics = false,
double? titleSpacing,
double? collapsedHeight,
double? expandedHeight,
bool floating = false,
bool pinned = true,
bool snap = false,
bool stretch = false,
double stretchTriggerOffset = 100.0,
AsyncCallback? onStretchTrigger,
ShapeBorder? shape,
double toolbarHeight = _MediumScrollUnderFlexibleConfig.collapsedHeight,
double? leadingWidth,
TextStyle? toolbarTextStyle,
TextStyle? titleTextStyle,
SystemUiOverlayStyle? systemOverlayStyle,
}) {
return SliverAppBar(
key: key,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
hasLeading: leading != null,
title: title,
actions: actions,
foregroundColor: foregroundColor,
variant: _ScrollUnderFlexibleVariant.medium,
centerCollapsedTitle: centerTitle,
primary: primary,
leadingWidth: leadingWidth,
titleSpacing: titleSpacing,
),
bottom: bottom,
elevation: elevation,
scrolledUnderElevation: scrolledUnderElevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
forceElevated: forceElevated,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
primary: primary,
centerTitle: centerTitle,
excludeHeaderSemantics: excludeHeaderSemantics,
titleSpacing: titleSpacing,
collapsedHeight: collapsedHeight ?? _MediumScrollUnderFlexibleConfig.collapsedHeight,
expandedHeight: expandedHeight ?? _MediumScrollUnderFlexibleConfig.expandedHeight,
floating: floating,
pinned: pinned,
snap: snap,
stretch: stretch,
stretchTriggerOffset: stretchTriggerOffset,
onStretchTrigger: onStretchTrigger,
shape: shape,
toolbarHeight: toolbarHeight,
leadingWidth: leadingWidth,
toolbarTextStyle: toolbarTextStyle,
titleTextStyle: titleTextStyle,
systemOverlayStyle: systemOverlayStyle,
);
}
const SliverAppBar.medium({
super.key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.flexibleSpace,
this.bottom,
this.elevation,
this.scrolledUnderElevation,
this.shadowColor,
this.surfaceTintColor,
this.forceElevated = false,
this.backgroundColor,
this.foregroundColor,
this.iconTheme,
this.actionsIconTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
this.titleSpacing,
this.collapsedHeight,
this.expandedHeight,
this.floating = false,
this.pinned = true,
this.snap = false,
this.stretch = false,
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
this.shape,
this.toolbarHeight = _MediumScrollUnderFlexibleConfig.collapsedHeight,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.clipBehavior,
}) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0),
assert(
collapsedHeight == null || collapsedHeight >= toolbarHeight,
'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].',
),
_variant = _SliverAppVariant.medium;
/// Creates a Material Design large top app bar that can be placed
/// in a [CustomScrollView].
......@@ -1604,87 +1586,50 @@ class SliverAppBar extends StatefulWidget {
/// * [SliverAppBar.medium], for a medium top app bar.
/// * https://m3.material.io/components/top-app-bar/overview, the Material 3
/// app bar specification.
factory SliverAppBar.large({
Key? key,
Widget? leading,
bool automaticallyImplyLeading = true,
Widget? title,
List<Widget>? actions,
Widget? flexibleSpace,
PreferredSizeWidget? bottom,
double? elevation,
double? scrolledUnderElevation,
Color? shadowColor,
Color? surfaceTintColor,
bool forceElevated = false,
Color? backgroundColor,
Color? foregroundColor,
IconThemeData? iconTheme,
IconThemeData? actionsIconTheme,
bool primary = true,
bool? centerTitle,
bool excludeHeaderSemantics = false,
double? titleSpacing,
double? collapsedHeight,
double? expandedHeight,
bool floating = false,
bool pinned = true,
bool snap = false,
bool stretch = false,
double stretchTriggerOffset = 100.0,
AsyncCallback? onStretchTrigger,
ShapeBorder? shape,
double toolbarHeight = _LargeScrollUnderFlexibleConfig.collapsedHeight,
double? leadingWidth,
TextStyle? toolbarTextStyle,
TextStyle? titleTextStyle,
SystemUiOverlayStyle? systemOverlayStyle,
}) {
return SliverAppBar(
key: key,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
hasLeading: leading != null,
title: title,
actions: actions,
foregroundColor: foregroundColor,
variant: _ScrollUnderFlexibleVariant.large,
centerCollapsedTitle: centerTitle,
primary: primary,
leadingWidth: leadingWidth,
titleSpacing: titleSpacing,
),
bottom: bottom,
elevation: elevation,
scrolledUnderElevation: scrolledUnderElevation,
shadowColor: shadowColor,
surfaceTintColor: surfaceTintColor,
forceElevated: forceElevated,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
primary: primary,
centerTitle: centerTitle,
excludeHeaderSemantics: excludeHeaderSemantics,
titleSpacing: titleSpacing,
collapsedHeight: collapsedHeight ?? _LargeScrollUnderFlexibleConfig.collapsedHeight,
expandedHeight: expandedHeight ?? _LargeScrollUnderFlexibleConfig.expandedHeight,
floating: floating,
pinned: pinned,
snap: snap,
stretch: stretch,
stretchTriggerOffset: stretchTriggerOffset,
onStretchTrigger: onStretchTrigger,
shape: shape,
toolbarHeight: toolbarHeight,
leadingWidth: leadingWidth,
toolbarTextStyle: toolbarTextStyle,
titleTextStyle: titleTextStyle,
systemOverlayStyle: systemOverlayStyle,
);
}
const SliverAppBar.large({
super.key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.flexibleSpace,
this.bottom,
this.elevation,
this.scrolledUnderElevation,
this.shadowColor,
this.surfaceTintColor,
this.forceElevated = false,
this.backgroundColor,
this.foregroundColor,
this.iconTheme,
this.actionsIconTheme,
this.primary = true,
this.centerTitle,
this.excludeHeaderSemantics = false,
this.titleSpacing,
this.collapsedHeight,
this.expandedHeight,
this.floating = false,
this.pinned = true,
this.snap = false,
this.stretch = false,
this.stretchTriggerOffset = 100.0,
this.onStretchTrigger,
this.shape,
this.toolbarHeight = _LargeScrollUnderFlexibleConfig.collapsedHeight,
this.leadingWidth,
this.toolbarTextStyle,
this.titleTextStyle,
this.systemOverlayStyle,
this.forceMaterialTransparency = false,
this.clipBehavior,
}) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0),
assert(
collapsedHeight == null || collapsedHeight >= toolbarHeight,
'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].',
),
_variant = _SliverAppVariant.large;
/// {@macro flutter.material.appbar.leading}
///
......@@ -1941,6 +1886,8 @@ class SliverAppBar extends StatefulWidget {
/// {@macro flutter.material.Material.clipBehavior}
final Clip? clipBehavior;
final _SliverAppVariant _variant;
@override
State<SliverAppBar> createState() => _SliverAppBarState();
}
......@@ -2004,6 +1951,41 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? (widget.collapsedHeight ?? 0.0) + bottomHeight + topPadding
: (widget.collapsedHeight ?? widget.toolbarHeight) + bottomHeight + topPadding;
final double? effectiveExpandedHeight;
final double effectiveCollapsedHeight;
final Widget? effectiveFlexibleSpace;
switch (widget._variant) {
case _SliverAppVariant.small:
effectiveExpandedHeight = widget.expandedHeight;
effectiveCollapsedHeight = collapsedHeight;
effectiveFlexibleSpace = widget.flexibleSpace;
case _SliverAppVariant.medium:
effectiveExpandedHeight = widget.expandedHeight
?? _MediumScrollUnderFlexibleConfig.expandedHeight + bottomHeight;
effectiveCollapsedHeight = widget.collapsedHeight
?? topPadding + _MediumScrollUnderFlexibleConfig.collapsedHeight + bottomHeight;
effectiveFlexibleSpace = widget.flexibleSpace ?? _ScrollUnderFlexibleSpace(
title: widget.title,
foregroundColor: widget.foregroundColor,
variant: _ScrollUnderFlexibleVariant.medium,
primary: widget.primary,
titleTextStyle: widget.titleTextStyle,
bottomHeight: bottomHeight,
);
case _SliverAppVariant.large:
effectiveExpandedHeight = widget.expandedHeight
?? _LargeScrollUnderFlexibleConfig.expandedHeight + bottomHeight;
effectiveCollapsedHeight = widget.collapsedHeight
?? topPadding + _LargeScrollUnderFlexibleConfig.collapsedHeight + bottomHeight;
effectiveFlexibleSpace = widget.flexibleSpace ?? _ScrollUnderFlexibleSpace(
title: widget.title,
foregroundColor: widget.foregroundColor,
variant: _ScrollUnderFlexibleVariant.large,
primary: widget.primary,
titleTextStyle: widget.titleTextStyle,
bottomHeight: bottomHeight,
);
}
return MediaQuery.removePadding(
context: context,
......@@ -2017,7 +1999,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title,
actions: widget.actions,
flexibleSpace: widget.flexibleSpace,
flexibleSpace: effectiveFlexibleSpace,
bottom: widget.bottom,
elevation: widget.elevation,
scrolledUnderElevation: widget.scrolledUnderElevation,
......@@ -2032,8 +2014,8 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing,
expandedHeight: widget.expandedHeight,
collapsedHeight: collapsedHeight,
expandedHeight: effectiveExpandedHeight,
collapsedHeight: effectiveCollapsedHeight,
topPadding: topPadding,
floating: widget.floating,
pinned: widget.pinned,
......@@ -2048,6 +2030,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency,
clipBehavior: widget.clipBehavior,
variant: widget._variant,
),
),
);
......@@ -2098,35 +2081,30 @@ enum _ScrollUnderFlexibleVariant { medium, large }
class _ScrollUnderFlexibleSpace extends StatelessWidget {
const _ScrollUnderFlexibleSpace({
required this.hasLeading,
this.title,
this.actions,
this.foregroundColor,
required this.variant,
this.centerCollapsedTitle,
this.primary = true,
this.leadingWidth,
this.titleSpacing,
this.titleTextStyle,
required this.bottomHeight,
});
final bool hasLeading;
final Widget? title;
final List<Widget>? actions;
final Color? foregroundColor;
final _ScrollUnderFlexibleVariant variant;
final bool? centerCollapsedTitle;
final bool primary;
final double? leadingWidth;
final double? titleSpacing;
final TextStyle? titleTextStyle;
final double bottomHeight;
@override
Widget build(BuildContext context) {
late final ThemeData theme = Theme.of(context);
late final AppBarTheme appBarTheme = AppBarTheme.of(context);
final AppBarTheme defaults = theme.useMaterial3 ? _AppBarDefaultsM3(context) : _AppBarDefaultsM2(context);
final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
final double topPadding = primary ? MediaQuery.viewPaddingOf(context).top : 0;
final double collapsedHeight = settings.minExtent - topPadding;
final double scrollUnderHeight = settings.maxExtent - settings.minExtent;
final double collapsedHeight = settings.minExtent - topPadding - bottomHeight;
final double scrollUnderHeight = settings.maxExtent - settings.minExtent + bottomHeight;
final _ScrollUnderFlexibleConfig config;
switch (variant) {
case _ScrollUnderFlexibleVariant.medium:
......@@ -2135,71 +2113,46 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget {
config = _LargeScrollUnderFlexibleConfig(context);
}
late final Widget? collapsedTitle;
late final Widget? expandedTitle;
if (title != null) {
collapsedTitle = config.collapsedTextStyle != null
? DefaultTextStyle(
style: config.collapsedTextStyle!.copyWith(color: foregroundColor ?? appBarTheme.foregroundColor),
child: title!,
)
: title;
final TextStyle? expandedTextStyle = titleTextStyle
?? appBarTheme.titleTextStyle
?? config.expandedTextStyle?.copyWith(
color: foregroundColor ?? appBarTheme.foregroundColor ?? defaults.foregroundColor,
);
expandedTitle = config.expandedTextStyle != null
? DefaultTextStyle(
style: config.expandedTextStyle!.copyWith(color: foregroundColor ?? appBarTheme.foregroundColor),
style: expandedTextStyle!,
child: title!,
)
: title;
}
late final bool centerTitle;
{
bool platformCenter() {
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return false;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return true;
}
EdgeInsetsGeometry expandedTitlePadding() {
final EdgeInsets padding = config.expandedTitlePadding!.resolve(Directionality.of(context));
if (bottomHeight > 0) {
return EdgeInsets.fromLTRB(padding.left, 0, padding.right, bottomHeight);
}
centerTitle = centerCollapsedTitle ?? appBarTheme.centerTitle ?? platformCenter();
}
EdgeInsetsGeometry effectiveCollapsedTitlePadding = EdgeInsets.zero;
if (hasLeading && leadingWidth == null) {
effectiveCollapsedTitlePadding = centerTitle
? config.collapsedCenteredTitlePadding!
: config.collapsedTitlePadding!;
} else if (hasLeading && leadingWidth != null) {
effectiveCollapsedTitlePadding = EdgeInsetsDirectional.only(start: leadingWidth!);
if (theme.useMaterial3) {
final TextStyle textStyle = config.expandedTextStyle!;
// Substract the bottom line height from the bottom padding.
// TODO(tahatesser): Figure out why this is done.
// https://github.com/flutter/flutter/issues/120672
final double adjustBottomPadding = padding.bottom
- (textStyle.fontSize! * textStyle.height! - textStyle.fontSize!) / 2;
return EdgeInsets.fromLTRB(
padding.left,
0,
padding.right,
adjustBottomPadding,
);
}
return padding;
}
final bool isCollapsed = settings.isScrolledUnder ?? false;
return Column(
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: topPadding),
child: Container(
height: collapsedHeight,
padding: effectiveCollapsedTitlePadding,
child: NavigationToolbar(
centerMiddle: centerTitle,
middleSpacing: titleSpacing ?? appBarTheme.titleSpacing ?? NavigationToolbar.kMiddleSpacing,
middle: AnimatedOpacity(
opacity: isCollapsed ? 1 : 0,
duration: const Duration(milliseconds: 500),
curve: const Cubic(0.2, 0.0, 0.0, 1.0),
child: collapsedTitle,
),
trailing: actions != null ? Row(
mainAxisSize: MainAxisSize.min,
children: actions!,
) : null,
),
),
padding: EdgeInsets.only(top: collapsedHeight + topPadding),
),
Flexible(
child: ClipRect(
......@@ -2209,7 +2162,7 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget {
alignment: Alignment.bottomLeft,
child: Container(
alignment: AlignmentDirectional.bottomStart,
padding: config.expandedTitlePadding,
padding: expandedTitlePadding(),
child: expandedTitle,
),
),
......@@ -2223,8 +2176,6 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget {
mixin _ScrollUnderFlexibleConfig {
TextStyle? get collapsedTextStyle;
TextStyle? get expandedTextStyle;
EdgeInsetsGeometry? get collapsedTitlePadding;
EdgeInsetsGeometry? get collapsedCenteredTitlePadding;
EdgeInsetsGeometry? get expandedTitlePadding;
}
......@@ -2332,12 +2283,6 @@ class _MediumScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
TextStyle? get expandedTextStyle =>
_textTheme.headlineSmall?.apply(color: _colors.onSurface);
@override
EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 20);
}
......@@ -2361,12 +2306,6 @@ class _LargeScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
TextStyle? get expandedTextStyle =>
_textTheme.headlineMedium?.apply(color: _colors.onSurface);
@override
EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsetsDirectional.only(start: 40);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 28);
}
......
......@@ -1086,16 +1086,18 @@ void main() {
});
testWidgets('SliverAppBar.medium defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 112;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
title: const Text('AppBar Title'),
const SliverAppBar.medium(
title: Text('AppBar Title'),
),
SliverToBoxAdapter(
child: Container(
......@@ -1109,21 +1111,20 @@ void main() {
));
final ScrollController controller = primaryScrollController(tester);
// There are two widgets for the title. The first is the title on the main
// row with the icons. It is transparent when the app bar is expanded, and
// opaque when it is collapsed. The second title is a larger version that is
// shown at the bottom when the app bar is expanded. It scrolls under the
// main row until it is completely hidden and then the first title is faded
// in.
final Finder collapsedTitle = find.text('AppBar Title').first;
final Finder collapsedTitleOpacity = find.ancestor(
of: collapsedTitle,
matching: find.byType(AnimatedOpacity),
);
final Finder expandedTitle = find.text('AppBar Title').last;
// There are two widgets for the title. The first title is a larger version
// that is shown at the bottom when the app bar is expanded. It scrolls under
// the main row until it is completely hidden and then the first title is
// faded in. The last is the title on the mainrow with the icons. It is
// transparent when the app bar is expanded, and opaque when it is collapsed.
final Finder expandedTitle = find.text('AppBar Title').first;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
).first;
final Finder collapsedTitle = find.text('AppBar Title').last;
final Finder collapsedTitleOpacity = find.ancestor(
of: collapsedTitle,
matching: find.byType(AnimatedOpacity),
);
// Default, fully expanded app bar.
......@@ -1133,6 +1134,17 @@ void main() {
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
// Test the expanded title is positioned correctly.
final Offset titleOffset = tester.getBottomLeft(expandedTitle);
expect(titleOffset.dx, 16.0);
expect(titleOffset.dy, closeTo(96.0, 0.1));
// Test the expanded title default color.
expect(
tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
theme.colorScheme.onSurface,
);
// Scroll the expanded app bar partially out of view.
controller.jumpTo(45);
await tester.pump();
......@@ -1159,16 +1171,18 @@ void main() {
});
testWidgets('SliverAppBar.large defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 152;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.large(
title: const Text('AppBar Title'),
const SliverAppBar.large(
title: Text('AppBar Title'),
),
SliverToBoxAdapter(
child: Container(
......@@ -1182,21 +1196,20 @@ void main() {
));
final ScrollController controller = primaryScrollController(tester);
// There are two widgets for the title. The first is the title on the main
// row with the icons. It is transparent when the app bar is expanded, and
// opaque when it is collapsed. The second title is a larger version that is
// shown at the bottom when the app bar is expanded. It scrolls under the
// main row until it is completely hidden and then the first title is faded
// in.
final Finder collapsedTitle = find.text('AppBar Title').first;
final Finder collapsedTitleOpacity = find.ancestor(
of: collapsedTitle,
matching: find.byType(AnimatedOpacity),
);
final Finder expandedTitle = find.text('AppBar Title').last;
// There are two widgets for the title. The first title is a larger version
// that is shown at the bottom when the app bar is expanded. It scrolls under
// the main row until it is completely hidden and then the first title is
// faded in. The last is the title on the mainrow with the icons. It is
// transparent when the app bar is expanded, and opaque when it is collapsed.
final Finder expandedTitle = find.text('AppBar Title').first;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
).first;
final Finder collapsedTitle = find.text('AppBar Title').last;
final Finder collapsedTitleOpacity = find.ancestor(
of: collapsedTitle,
matching: find.byType(AnimatedOpacity),
);
// Default, fully expanded app bar.
......@@ -1206,6 +1219,19 @@ void main() {
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
// Test the expanded title is positioned correctly.
final Offset titleOffset = tester.getBottomLeft(expandedTitle);
expect(titleOffset.dx, 16.0);
expect(titleOffset.dy, closeTo(128.0, 0.1));
// Test the expanded title default color.
expect(
tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
theme.colorScheme.onSurface,
);
// Scroll the expanded app bar partially out of view.
controller.jumpTo(45);
await tester.pump();
......@@ -4049,6 +4075,7 @@ void main() {
onPressed: () {},
),
title: const Text(title, maxLines: 1),
centerTitle: true,
actions: const <Widget>[
Icon(Icons.search),
Icon(Icons.sort),
......@@ -4073,11 +4100,11 @@ void main() {
await tester.pumpAndSettle();
final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu));
Offset titleOffset = tester.getTopLeft(find.text(title).first);
Offset titleOffset = tester.getTopLeft(find.text(title).last);
// The title widget should be to the right of the leading widget.
expect(titleOffset.dx, greaterThan(leadingOffset.dx));
titleOffset = tester.getTopRight(find.text(title).first);
titleOffset = tester.getTopRight(find.text(title).last);
final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
// The title widget should be to the left of the search icon.
expect(titleOffset.dx, lessThan(searchOffset.dx));
......@@ -4100,6 +4127,7 @@ void main() {
onPressed: () {},
),
title: const Text(title, maxLines: 1),
centerTitle: true,
actions: const <Widget>[
Icon(Icons.search),
Icon(Icons.sort),
......@@ -4124,11 +4152,11 @@ void main() {
await tester.pumpAndSettle();
final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu));
Offset titleOffset = tester.getTopLeft(find.text(title).first);
Offset titleOffset = tester.getTopLeft(find.text(title).last);
// The title widget should be to the right of the leading widget.
expect(titleOffset.dx, greaterThan(leadingOffset.dx));
titleOffset = tester.getTopRight(find.text(title).first);
titleOffset = tester.getTopRight(find.text(title).last);
final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
// The title widget should be to the left of the search icon.
expect(titleOffset.dx, lessThan(searchOffset.dx));
......@@ -4144,18 +4172,21 @@ void main() {
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
centerTitle: centerTitle,
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 200),
sliver: SliverAppBar.medium(
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title, maxLines: 1),
centerTitle: centerTitle,
titleSpacing: titleSpacing,
actions: <Widget>[
IconButton(onPressed: () {}, icon: const Icon(Icons.sort)),
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
],
),
title: const Text(title, maxLines: 1),
titleSpacing: titleSpacing,
actions: const <Widget>[
Icon(Icons.sort),
Icon(Icons.more_vert),
],
),
SliverToBoxAdapter(
child: Container(
......@@ -4171,55 +4202,57 @@ void main() {
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
// By default, title widget should be to the right of the
// leading widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(find.text(title).first);
Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing);
Offset titleOffset = tester.getTopLeft(collapsedTitle);
Offset iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);
await tester.pumpWidget(buildWidget(centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
// By default, title widget should be to the left of the first
// leading widget and title spacing should be respected.
titleOffset = tester.getTopRight(find.text(title).first);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort));
expect(titleOffset.dx, iconOffset.dx - titleSpacing);
// trailing widget and title spacing should be respected.
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);
// Test custom title spacing, set to 0.0.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
// The title widget should be to the right of the leading
// widget with no spacing.
titleOffset = tester.getTopLeft(find.text(title).first);
iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
expect(titleOffset.dx, iconOffset.dx);
titleOffset = tester.getTopLeft(collapsedTitle);
iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconButtonOffset.dx);
// Set centerTitle to true so the end of the title can reach
// the action widgets.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
// The title widget should be to the left of the first
// leading widget with no spacing.
titleOffset = tester.getTopRight(find.text(title).first);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort));
expect(titleOffset.dx, iconOffset.dx);
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx);
});
testWidgets('SliverAppBar.large respects title spacing', (WidgetTester tester) async {
......@@ -4232,18 +4265,21 @@ void main() {
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.large(
centerTitle: centerTitle,
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 200),
sliver: SliverAppBar.large(
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title, maxLines: 1),
centerTitle: centerTitle,
titleSpacing: titleSpacing,
actions: <Widget>[
IconButton(onPressed: () {}, icon: const Icon(Icons.sort)),
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
],
),
title: const Text(title, maxLines: 1),
titleSpacing: titleSpacing,
actions: const <Widget>[
Icon(Icons.sort),
Icon(Icons.more_vert),
],
),
SliverToBoxAdapter(
child: Container(
......@@ -4259,61 +4295,63 @@ void main() {
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(160);
await tester.pumpAndSettle();
// By default, title widget should be to the right of the leading
// widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(find.text(title).first);
Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing);
Offset titleOffset = tester.getTopLeft(collapsedTitle);
Offset iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);
await tester.pumpWidget(buildWidget(centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(160);
await tester.pumpAndSettle();
// By default, title widget should be to the right of the
// By default, title widget should be to the left of the
// leading widget and title spacing should be respected.
titleOffset = tester.getTopRight(find.text(title).first);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort));
expect(titleOffset.dx, iconOffset.dx - titleSpacing);
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);
// Test custom title spacing, set to 0.0.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(160);
await tester.pumpAndSettle();
// The title widget should be to the right of the leading
// widget with no spacing.
titleOffset = tester.getTopLeft(find.text(title).first);
iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
expect(titleOffset.dx, iconOffset.dx);
titleOffset = tester.getTopLeft(collapsedTitle);
iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconButtonOffset.dx);
// Set centerTitle to true so the end of the title can reach
// the action widgets.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(160);
await tester.pumpAndSettle();
// The title widget should be to the left of the first
// leading widget with no spacing.
titleOffset = tester.getTopRight(find.text(title).first);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort));
expect(titleOffset.dx, iconOffset.dx);
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx);
});
testWidgets(
'SliverAppBar.medium without the leading widget updates collapsed title padding',
(WidgetTester widgetTester) async {
(WidgetTester tester) async {
const String title = 'Medium SliverAppBar Title';
const double leadingPadding = 40.0;
const double leadingPadding = 56.0;
const double titleSpacing = 16.0;
Widget buildWidget({ bool showLeading = true }) {
......@@ -4323,6 +4361,7 @@ void main() {
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
automaticallyImplyLeading: false,
leading: showLeading
? IconButton(
icon: const Icon(Icons.menu),
......@@ -4343,36 +4382,38 @@ void main() {
);
}
await widgetTester.pumpWidget(buildWidget());
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(widgetTester);
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
await widgetTester.pumpAndSettle();
await tester.pumpAndSettle();
// If the leading widget is present, the title widget should be to the
// right of the leading widget and title spacing should be respected.
Offset titleOffset = widgetTester.getTopLeft(find.text(title).first);
Offset titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, leadingPadding + titleSpacing);
// Hide the leading widget.
await widgetTester.pumpWidget(buildWidget(showLeading: false));
await tester.pumpWidget(buildWidget(showLeading: false));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(widgetTester);
controller = primaryScrollController(tester);
controller.jumpTo(45);
await widgetTester.pumpAndSettle();
await tester.pumpAndSettle();
// If the leading widget is not present, the title widget will
// only have the default title spacing.
titleOffset = widgetTester.getTopLeft(find.text(title).first);
titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, titleSpacing);
});
testWidgets(
'SliverAppBar.large without the leading widget updates collapsed title padding',
(WidgetTester widgetTester) async {
(WidgetTester tester) async {
const String title = 'Large SliverAppBar Title';
const double leadingPadding = 40.0;
const double leadingPadding = 56.0;
const double titleSpacing = 16.0;
Widget buildWidget({ bool showLeading = true }) {
......@@ -4382,6 +4423,7 @@ void main() {
primary: true,
slivers: <Widget>[
SliverAppBar.large(
automaticallyImplyLeading: false,
leading: showLeading
? IconButton(
icon: const Icon(Icons.menu),
......@@ -4402,31 +4444,211 @@ void main() {
);
}
await widgetTester.pumpWidget(buildWidget());
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll CustomScrollView to collapse SliverAppBar.
ScrollController controller = primaryScrollController(widgetTester);
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
await widgetTester.pumpAndSettle();
await tester.pumpAndSettle();
// If the leading widget is present, the title widget should be to the
// right of the leading widget and title spacing should be respected.
Offset titleOffset = widgetTester.getTopLeft(find.text(title).first);
Offset titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, leadingPadding + titleSpacing);
// Hide the leading widget.
await widgetTester.pumpWidget(buildWidget(showLeading: false));
await tester.pumpWidget(buildWidget(showLeading: false));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(widgetTester);
controller = primaryScrollController(tester);
controller.jumpTo(45);
await widgetTester.pumpAndSettle();
await tester.pumpAndSettle();
// If the leading widget is not present, the title widget will
// only have the default title spacing.
titleOffset = widgetTester.getTopLeft(find.text(title).first);
titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, titleSpacing);
});
testWidgets(
'SliverAppBar large & medium title respects automaticallyImplyLeading',
(WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/121511
const String title = 'AppBar Title';
const double titleSpacing = 16.0;
Widget buildWidget() {
return MaterialApp(
home: Scaffold(
body: Builder(
builder: (BuildContext context) {
return Center(
child: FilledButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute<void>(
builder: (BuildContext context) {
return Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
const SliverAppBar.large(
title: Text(title),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
);
},
));
},
child: const Text('Go to page'),
),
);
}
),
),
);
}
await tester.pumpWidget(buildWidget());
expect(find.byType(BackButton), findsNothing);
await tester.tap(find.byType(FilledButton));
await tester.pumpAndSettle();
final Finder collapsedTitle = find.text(title).last;
final Offset backButtonOffset = tester.getTopRight(find.byType(BackButton));
final Offset titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, backButtonOffset.dx + titleSpacing);
});
testWidgets('SliverAppBar.medium with bottom widget', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/115091
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 112;
const double bottomHeight = 48;
const String title = 'Medium App Bar';
Widget buildWidget() {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title),
bottom: const TabBar(
tabs: <Widget>[
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
),
);
}
await tester.pumpWidget(buildWidget());
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight + bottomHeight);
final Finder expandedTitle = find.text(title).first;
final Offset expandedTitleOffset = tester.getBottomLeft(expandedTitle);
final Offset tabOffset = tester.getTopLeft(find.byType(TabBar));
expect(expandedTitleOffset.dy, tabOffset.dy);
// Scroll CustomScrollView to collapse SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(160);
await tester.pumpAndSettle();
expect(appBarHeight(tester), collapsedAppBarHeight + bottomHeight);
});
testWidgets('SliverAppBar.large with bottom widget', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/115091
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 152;
const double bottomHeight = 48;
const String title = 'Large App Bar';
Widget buildWidget() {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.large(
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title),
bottom: const TabBar(
tabs: <Widget>[
Tab(text: 'Tab 1'),
Tab(text: 'Tab 2'),
Tab(text: 'Tab 3'),
],
),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
),
);
}
await tester.pumpWidget(buildWidget());
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight + bottomHeight);
final Finder expandedTitle = find.text(title).first;
final Offset expandedTitleOffset = tester.getBottomLeft(expandedTitle);
final Offset tabOffset = tester.getTopLeft(find.byType(TabBar));
expect(expandedTitleOffset.dy, tabOffset.dy);
// Scroll CustomScrollView to collapse SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(200);
await tester.pumpAndSettle();
expect(appBarHeight(tester), collapsedAppBarHeight + bottomHeight);
});
group('AppBar.forceMaterialTransparency', () {
Material getAppBarMaterial(WidgetTester tester) {
return tester.widget<Material>(find
......@@ -4501,4 +4723,177 @@ void main() {
expect(buttonWasPressed, isFalse);
});
});
group('Material 2', () {
// Tests that are only relevant for Material 2. Once ThemeData.useMaterial3
// is turned on by default, these tests can be removed.
testWidgets('SliverAppBar.medium defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 112;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
const SliverAppBar.medium(
title: Text('AppBar Title'),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
));
final ScrollController controller = primaryScrollController(tester);
// There are two widgets for the title. The first title is a larger version
// that is shown at the bottom when the app bar is expanded. It scrolls under
// the main row until it is completely hidden and then the first title is
// faded in. The last is the title on the mainrow with the icons. It is
// transparent when the app bar is expanded, and opaque when it is collapsed.
final Finder expandedTitle = find.text('AppBar Title').first;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
);
final Finder collapsedTitle = find.text('AppBar Title').last;
final Finder collapsedTitleOpacity = find.ancestor(
of: collapsedTitle,
matching: find.byType(AnimatedOpacity),
);
// Default, fully expanded app bar.
expect(controller.offset, 0);
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
// Test the expanded title is positioned correctly.
final Offset titleOffset = tester.getBottomLeft(expandedTitle);
expect(titleOffset, const Offset(16.0, 92.0));
// Test the expanded title default color.
expect(
tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
theme.colorScheme.onPrimary,
);
// Scroll the expanded app bar partially out of view.
controller.jumpTo(45);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight - 45);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45);
// Scroll so that it is completely collapsed.
controller.jumpTo(600);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), collapsedAppBarHeight);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 1);
expect(tester.getSize(expandedTitleClip).height, 0);
// Scroll back to fully expanded.
controller.jumpTo(0);
await tester.pumpAndSettle();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
});
testWidgets('SliverAppBar.large defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: false);
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 152;
await tester.pumpWidget(MaterialApp(
theme: ThemeData(useMaterial3: false),
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
const SliverAppBar.large(
title: Text('AppBar Title'),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
));
final ScrollController controller = primaryScrollController(tester);
// There are two widgets for the title. The first title is a larger version
// that is shown at the bottom when the app bar is expanded. It scrolls under
// the main row until it is completely hidden and then the first title is
// faded in. The last is the title on the mainrow with the icons. It is
// transparent when the app bar is expanded, and opaque when it is collapsed.
final Finder expandedTitle = find.text('AppBar Title').first;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
);
final Finder collapsedTitle = find.text('AppBar Title').last;
final Finder collapsedTitleOpacity = find.ancestor(
of: collapsedTitle,
matching: find.byType(AnimatedOpacity),
);
// Default, fully expanded app bar.
expect(controller.offset, 0);
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
// Test the expanded title is positioned correctly.
final Offset titleOffset = tester.getBottomLeft(expandedTitle);
expect(titleOffset, const Offset(16.0, 124.0));
// Test the expanded title default color.
expect(
tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
theme.colorScheme.onPrimary,
);
// Scroll the expanded app bar partially out of view.
controller.jumpTo(45);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight - 45);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight - 45);
// Scroll so that it is completely collapsed.
controller.jumpTo(600);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), collapsedAppBarHeight);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 1);
expect(tester.getSize(expandedTitleClip).height, 0);
// Scroll back to fully expanded.
controller.jumpTo(0);
await tester.pumpAndSettle();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight);
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight);
});
});
}
......@@ -9,6 +9,25 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
const AppBarTheme appBarTheme = AppBarTheme(
backgroundColor: Color(0xff00ff00),
foregroundColor: Color(0xff00ffff),
elevation: 4.0,
scrolledUnderElevation: 6.0,
shadowColor: Color(0xff1212ff),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14.0)),
),
iconTheme: IconThemeData(color: Color(0xffff0000)),
actionsIconTheme: IconThemeData(color: Color(0xff0000ff)),
centerTitle: false,
titleSpacing: 10.0,
titleTextStyle: TextStyle(
fontSize: 22.0,
fontStyle: FontStyle.italic,
),
);
ScrollController primaryScrollController(WidgetTester tester) {
return PrimaryScrollController.of(tester.element(find.byType(CustomScrollView)));
}
......@@ -681,18 +700,10 @@ void main() {
});
testWidgets('SliverAppBar.medium uses AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Medium SliverAppBar Title';
const Color foregroundColor = Color(0xff00ff00);
const double titleSpacing = 10.0;
const String title = 'Medium App Bar';
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
appBarTheme: const AppBarTheme(
foregroundColor: foregroundColor,
titleSpacing: titleSpacing,
centerTitle: false,
),
),
theme: ThemeData(appBarTheme: appBarTheme),
home: CustomScrollView(
primary: true,
slivers: <Widget>[
......@@ -702,82 +713,130 @@ void main() {
icon: const Icon(Icons.menu),
),
title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
),
],
),
));
final RichText text = tester.firstWidget(find.byType(RichText));
expect(text.text.style!.color, foregroundColor);
// Test title.
final RichText titleText = tester.firstWidget(find.byType(RichText));
expect(titleText.text.style!.fontSize, appBarTheme.titleTextStyle!.fontSize);
expect(titleText.text.style!.fontStyle, appBarTheme.titleTextStyle!.fontStyle);
// Test background color, shadow color, and shape.
final Material material = tester.widget<Material>(
find.descendant(
of: find.byType(SliverAppBar),
matching: find.byType(Material).first,
),
);
expect(material.color, appBarTheme.backgroundColor);
expect(material.shadowColor, appBarTheme.shadowColor);
expect(material.shape, appBarTheme.shape);
final RichText actionIcon = tester.widget(find.byType(RichText).last);
expect(actionIcon.text.style!.color, appBarTheme.actionsIconTheme!.color);
// Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first);
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
// Title spacing should be 10.0.
expect(titleOffset.dx, iconOffset.dx + titleSpacing);
// Test title spacing.
final Finder collapsedTitle = find.text(title).last;
final Offset titleOffset = tester.getTopLeft(collapsedTitle);
final Offset iconOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + appBarTheme.titleSpacing!);
});
testWidgets('SliverAppBar.medium properties take priority over AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Medium SliverAppBar Title';
const Color foregroundColor = Color(0xff00ff00);
const double titleSpacing = 10.0;
const String title = 'Medium App Bar';
const Color backgroundColor = Color(0xff000099);
const Color foregroundColor = Color(0xff00ff98);
const Color shadowColor = Color(0xff00ff97);
const ShapeBorder shape = RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.only(bottomStart: Radius.circular(12.0)),
);
const IconThemeData iconTheme = IconThemeData(color: Color(0xff00ff96));
const IconThemeData actionsIconTheme = IconThemeData(color: Color(0xff00ff95));
const double titleSpacing = 18.0;
const TextStyle titleTextStyle = TextStyle(
fontSize: 22.9,
fontStyle: FontStyle.italic,
);
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
appBarTheme: const AppBarTheme(
foregroundColor: Color(0xffff0000),
titleSpacing: 14.0,
centerTitle: true,
),
),
theme: ThemeData(appBarTheme: appBarTheme),
home: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
centerTitle: false,
titleSpacing: titleSpacing,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
shadowColor: shadowColor,
shape: shape,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
titleSpacing: titleSpacing,
titleTextStyle: titleTextStyle,
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
),
],
),
));
final RichText text = tester.firstWidget(find.byType(RichText));
expect(text.text.style!.color, foregroundColor);
// Test title.
final RichText titleText = tester.firstWidget(find.byType(RichText));
expect(titleText.text.style, titleTextStyle);
// Test background color, shadow color, and shape.
final Material material = tester.widget<Material>(
find.descendant(
of: find.byType(SliverAppBar),
matching: find.byType(Material).first,
),
);
expect(material.color, backgroundColor);
expect(material.shadowColor, shadowColor);
expect(material.shape, shape);
final RichText actionIcon = tester.widget(find.byType(RichText).last);
expect(actionIcon.text.style!.color, actionsIconTheme.color);
// Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first);
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
// Title spacing should be 10.0.
// Test title spacing.
final Finder collapsedTitle = find.text(title).last;
final Offset titleOffset = tester.getTopLeft(collapsedTitle);
final Offset iconOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing);
});
testWidgets('SliverAppBar.large uses AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Large SliverAppBar Title';
const Color foregroundColor = Color(0xff00ff00);
const double titleSpacing = 10.0;
const String title = 'Large App Bar';
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
appBarTheme: const AppBarTheme(
foregroundColor: foregroundColor,
titleSpacing: titleSpacing,
centerTitle: false,
),
),
theme: ThemeData(appBarTheme: appBarTheme),
home: CustomScrollView(
primary: true,
slivers: <Widget>[
......@@ -787,69 +846,169 @@ void main() {
icon: const Icon(Icons.menu),
),
title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
),
],
),
));
final RichText text = tester.firstWidget(find.byType(RichText));
expect(text.text.style!.color, foregroundColor);
// Test title.
final RichText titleText = tester.firstWidget(find.byType(RichText));
expect(titleText.text.style!.fontSize, appBarTheme.titleTextStyle!.fontSize);
expect(titleText.text.style!.fontStyle, appBarTheme.titleTextStyle!.fontStyle);
// Test background color, shadow color, and shape.
final Material material = tester.widget<Material>(
find.descendant(
of: find.byType(SliverAppBar),
matching: find.byType(Material).first,
),
);
expect(material.color, appBarTheme.backgroundColor);
expect(material.shadowColor, appBarTheme.shadowColor);
expect(material.shape, appBarTheme.shape);
final RichText actionIcon = tester.widget(find.byType(RichText).last);
expect(actionIcon.text.style!.color, appBarTheme.actionsIconTheme!.color);
// Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first);
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
// Title spacing should be 10.0.
expect(titleOffset.dx, iconOffset.dx + titleSpacing);
// Test title spacing.
final Finder collapsedTitle = find.text(title).last;
final Offset titleOffset = tester.getTopLeft(collapsedTitle);
final Offset iconOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + appBarTheme.titleSpacing!);
});
testWidgets('SliverAppBar.large properties take priority over AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Large SliverAppBar Title';
const Color foregroundColor = Color(0xff00ff00);
const double titleSpacing = 10.0;
const String title = 'Large App Bar';
const Color backgroundColor = Color(0xff000099);
const Color foregroundColor = Color(0xff00ff98);
const Color shadowColor = Color(0xff00ff97);
const ShapeBorder shape = RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.only(bottomStart: Radius.circular(12.0)),
);
const IconThemeData iconTheme = IconThemeData(color: Color(0xff00ff96));
const IconThemeData actionsIconTheme = IconThemeData(color: Color(0xff00ff95));
const double titleSpacing = 18.0;
const TextStyle titleTextStyle = TextStyle(
fontSize: 22.9,
fontStyle: FontStyle.italic,
);
await tester.pumpWidget(MaterialApp(
theme: ThemeData(
appBarTheme: const AppBarTheme(
foregroundColor: Color(0xffff0000),
titleSpacing: 14.0,
centerTitle: true,
),
),
theme: ThemeData(appBarTheme: appBarTheme),
home: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.large(
centerTitle: false,
titleSpacing: titleSpacing,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
shadowColor: shadowColor,
shape: shape,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
titleSpacing: titleSpacing,
titleTextStyle: titleTextStyle,
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
),
],
),
));
final RichText text = tester.firstWidget(find.byType(RichText));
expect(text.text.style!.color, foregroundColor);
// Test title.
final RichText titleText = tester.firstWidget(find.byType(RichText));
expect(titleText.text.style, titleTextStyle);
// Test background color, shadow color, and shape.
final Material material = tester.widget<Material>(
find.descendant(
of: find.byType(SliverAppBar),
matching: find.byType(Material).first,
),
);
expect(material.color, backgroundColor);
expect(material.shadowColor, shadowColor);
expect(material.shape, shape);
final RichText actionIcon = tester.widget(find.byType(RichText).last);
expect(actionIcon.text.style!.color, actionsIconTheme.color);
// Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
controller.jumpTo(120);
await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first);
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu));
// Title spacing should be 10.0.
// Test title spacing.
final Finder collapsedTitle = find.text(title).last;
final Offset titleOffset = tester.getTopLeft(collapsedTitle);
final Offset iconOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing);
});
testWidgets(
'SliverAppBar medium & large supports foregroundColor', (WidgetTester tester) async {
const String title = 'AppBar title';
const AppBarTheme appBarTheme = AppBarTheme(foregroundColor: Color(0xff00ff20));
const Color foregroundColor = Color(0xff001298);
Widget buildWidget({ Color? color, AppBarTheme? appBarTheme }) {
return MaterialApp(
theme: ThemeData(appBarTheme: appBarTheme),
home: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
foregroundColor: color,
title: const Text(title),
),
SliverAppBar.large(
foregroundColor: color,
title: const Text(title),
),
],
),
);
}
await tester.pumpWidget(buildWidget(appBarTheme: appBarTheme));
// Test AppBarTheme.foregroundColor parameter.
RichText mediumTitle = tester.widget(find.byType(RichText).first);
expect(mediumTitle.text.style!.color, appBarTheme.foregroundColor);
RichText largeTitle = tester.widget(find.byType(RichText).first);
expect(largeTitle.text.style!.color, appBarTheme.foregroundColor);
await tester.pumpWidget(buildWidget(
color: foregroundColor, appBarTheme: appBarTheme),
);
// Test foregroundColor parameter.
mediumTitle = tester.widget(find.byType(RichText).first);
expect(mediumTitle.text.style!.color, foregroundColor);
largeTitle = tester.widget(find.byType(RichText).first);
expect(largeTitle.text.style!.color, foregroundColor);
});
testWidgets('Default AppBarTheme debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const AppBarTheme().debugFillProperties(builder);
......
......@@ -2720,8 +2720,8 @@ void main() {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.medium(
title: const Text('AppBar Title'),
sliver: const SliverAppBar.medium(
title: Text('AppBar Title'),
),
),
];
......@@ -2747,11 +2747,11 @@ void main() {
));
// There are two widgets for the title.
final Finder expandedTitle = find.text('AppBar Title').last;
final Finder expandedTitle = find.text('AppBar Title').first;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
);
).first;
// Default, fully expanded app bar.
expect(nestedScrollView.currentState?.outerController.offset, 0);
......@@ -2830,11 +2830,11 @@ void main() {
));
// There are two widgets for the title.
final Finder expandedTitle = find.text('AppBar Title').last;
final Finder expandedTitle = find.text('AppBar Title').first;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
);
).first;
// Default, fully expanded app bar.
expect(nestedScrollView.currentState?.outerController.offset, 0);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment