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'; ...@@ -34,6 +34,8 @@ import 'theme.dart';
const double _kLeadingWidth = kToolbarHeight; // So the leading button is square. 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. 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. // Bottom justify the toolbarHeight child which may overflow the top.
class _ToolbarContainerLayout extends SingleChildLayoutDelegate { class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
const _ToolbarContainerLayout(this.toolbarHeight); const _ToolbarContainerLayout(this.toolbarHeight);
...@@ -1191,7 +1193,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -1191,7 +1193,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
required this.titleTextStyle, required this.titleTextStyle,
required this.systemOverlayStyle, required this.systemOverlayStyle,
required this.forceMaterialTransparency, required this.forceMaterialTransparency,
required this.clipBehavior required this.clipBehavior,
required this.variant,
}) : assert(primary || topPadding == 0.0), }) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize.height ?? 0.0; _bottomHeight = bottom?.preferredSize.height ?? 0.0;
...@@ -1228,6 +1231,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -1228,6 +1231,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double _bottomHeight; final double _bottomHeight;
final bool forceMaterialTransparency; final bool forceMaterialTransparency;
final Clip? clipBehavior; final Clip? clipBehavior;
final _SliverAppVariant variant;
@override @override
double get minExtent => collapsedHeight; double get minExtent => collapsedHeight;
...@@ -1258,6 +1262,17 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -1258,6 +1262,17 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final double toolbarOpacity = !pinned || isPinnedWithOpacityFade final double toolbarOpacity = !pinned || isPinnedWithOpacityFade
? clampDouble(visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight), 0.0, 1.0) ? clampDouble(visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight), 0.0, 1.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( final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent, minExtent: minExtent,
...@@ -1269,7 +1284,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -1269,7 +1284,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
leading: leading, leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading, automaticallyImplyLeading: automaticallyImplyLeading,
title: title, title: effectiveTitle,
actions: actions, actions: actions,
flexibleSpace: (title == null && flexibleSpace != null && !excludeHeaderSemantics) flexibleSpace: (title == null && flexibleSpace != null && !excludeHeaderSemantics)
? Semantics( ? Semantics(
...@@ -1474,7 +1489,11 @@ class SliverAppBar extends StatefulWidget { ...@@ -1474,7 +1489,11 @@ class SliverAppBar extends StatefulWidget {
this.clipBehavior, this.clipBehavior,
}) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'), }) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0), 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 /// Creates a Material Design medium top app bar that can be placed
/// in a [CustomScrollView]. /// in a [CustomScrollView].
...@@ -1499,87 +1518,50 @@ class SliverAppBar extends StatefulWidget { ...@@ -1499,87 +1518,50 @@ class SliverAppBar extends StatefulWidget {
/// * [SliverAppBar.large], for a large top app bar. /// * [SliverAppBar.large], for a large top app bar.
/// * https://m3.material.io/components/top-app-bar/overview, the Material 3 /// * https://m3.material.io/components/top-app-bar/overview, the Material 3
/// app bar specification. /// app bar specification.
factory SliverAppBar.medium({ const SliverAppBar.medium({
Key? key, super.key,
Widget? leading, this.leading,
bool automaticallyImplyLeading = true, this.automaticallyImplyLeading = true,
Widget? title, this.title,
List<Widget>? actions, this.actions,
Widget? flexibleSpace, this.flexibleSpace,
PreferredSizeWidget? bottom, this.bottom,
double? elevation, this.elevation,
double? scrolledUnderElevation, this.scrolledUnderElevation,
Color? shadowColor, this.shadowColor,
Color? surfaceTintColor, this.surfaceTintColor,
bool forceElevated = false, this.forceElevated = false,
Color? backgroundColor, this.backgroundColor,
Color? foregroundColor, this.foregroundColor,
IconThemeData? iconTheme, this.iconTheme,
IconThemeData? actionsIconTheme, this.actionsIconTheme,
bool primary = true, this.primary = true,
bool? centerTitle, this.centerTitle,
bool excludeHeaderSemantics = false, this.excludeHeaderSemantics = false,
double? titleSpacing, this.titleSpacing,
double? collapsedHeight, this.collapsedHeight,
double? expandedHeight, this.expandedHeight,
bool floating = false, this.floating = false,
bool pinned = true, this.pinned = true,
bool snap = false, this.snap = false,
bool stretch = false, this.stretch = false,
double stretchTriggerOffset = 100.0, this.stretchTriggerOffset = 100.0,
AsyncCallback? onStretchTrigger, this.onStretchTrigger,
ShapeBorder? shape, this.shape,
double toolbarHeight = _MediumScrollUnderFlexibleConfig.collapsedHeight, this.toolbarHeight = _MediumScrollUnderFlexibleConfig.collapsedHeight,
double? leadingWidth, this.leadingWidth,
TextStyle? toolbarTextStyle, this.toolbarTextStyle,
TextStyle? titleTextStyle, this.titleTextStyle,
SystemUiOverlayStyle? systemOverlayStyle, this.systemOverlayStyle,
}) { this.forceMaterialTransparency = false,
return SliverAppBar( this.clipBehavior,
key: key, }) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
leading: leading, assert(stretchTriggerOffset > 0.0),
automaticallyImplyLeading: automaticallyImplyLeading, assert(
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace( collapsedHeight == null || collapsedHeight >= toolbarHeight,
hasLeading: leading != null, 'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].',
title: title, ),
actions: actions, _variant = _SliverAppVariant.medium;
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,
);
}
/// Creates a Material Design large top app bar that can be placed /// Creates a Material Design large top app bar that can be placed
/// in a [CustomScrollView]. /// in a [CustomScrollView].
...@@ -1604,87 +1586,50 @@ class SliverAppBar extends StatefulWidget { ...@@ -1604,87 +1586,50 @@ class SliverAppBar extends StatefulWidget {
/// * [SliverAppBar.medium], for a medium top app bar. /// * [SliverAppBar.medium], for a medium top app bar.
/// * https://m3.material.io/components/top-app-bar/overview, the Material 3 /// * https://m3.material.io/components/top-app-bar/overview, the Material 3
/// app bar specification. /// app bar specification.
factory SliverAppBar.large({ const SliverAppBar.large({
Key? key, super.key,
Widget? leading, this.leading,
bool automaticallyImplyLeading = true, this.automaticallyImplyLeading = true,
Widget? title, this.title,
List<Widget>? actions, this.actions,
Widget? flexibleSpace, this.flexibleSpace,
PreferredSizeWidget? bottom, this.bottom,
double? elevation, this.elevation,
double? scrolledUnderElevation, this.scrolledUnderElevation,
Color? shadowColor, this.shadowColor,
Color? surfaceTintColor, this.surfaceTintColor,
bool forceElevated = false, this.forceElevated = false,
Color? backgroundColor, this.backgroundColor,
Color? foregroundColor, this.foregroundColor,
IconThemeData? iconTheme, this.iconTheme,
IconThemeData? actionsIconTheme, this.actionsIconTheme,
bool primary = true, this.primary = true,
bool? centerTitle, this.centerTitle,
bool excludeHeaderSemantics = false, this.excludeHeaderSemantics = false,
double? titleSpacing, this.titleSpacing,
double? collapsedHeight, this.collapsedHeight,
double? expandedHeight, this.expandedHeight,
bool floating = false, this.floating = false,
bool pinned = true, this.pinned = true,
bool snap = false, this.snap = false,
bool stretch = false, this.stretch = false,
double stretchTriggerOffset = 100.0, this.stretchTriggerOffset = 100.0,
AsyncCallback? onStretchTrigger, this.onStretchTrigger,
ShapeBorder? shape, this.shape,
double toolbarHeight = _LargeScrollUnderFlexibleConfig.collapsedHeight, this.toolbarHeight = _LargeScrollUnderFlexibleConfig.collapsedHeight,
double? leadingWidth, this.leadingWidth,
TextStyle? toolbarTextStyle, this.toolbarTextStyle,
TextStyle? titleTextStyle, this.titleTextStyle,
SystemUiOverlayStyle? systemOverlayStyle, this.systemOverlayStyle,
}) { this.forceMaterialTransparency = false,
return SliverAppBar( this.clipBehavior,
key: key, }) : assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
leading: leading, assert(stretchTriggerOffset > 0.0),
automaticallyImplyLeading: automaticallyImplyLeading, assert(
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace( collapsedHeight == null || collapsedHeight >= toolbarHeight,
hasLeading: leading != null, 'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].',
title: title, ),
actions: actions, _variant = _SliverAppVariant.large;
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,
);
}
/// {@macro flutter.material.appbar.leading} /// {@macro flutter.material.appbar.leading}
/// ///
...@@ -1941,6 +1886,8 @@ class SliverAppBar extends StatefulWidget { ...@@ -1941,6 +1886,8 @@ class SliverAppBar extends StatefulWidget {
/// {@macro flutter.material.Material.clipBehavior} /// {@macro flutter.material.Material.clipBehavior}
final Clip? clipBehavior; final Clip? clipBehavior;
final _SliverAppVariant _variant;
@override @override
State<SliverAppBar> createState() => _SliverAppBarState(); State<SliverAppBar> createState() => _SliverAppBarState();
} }
...@@ -2004,6 +1951,41 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -2004,6 +1951,41 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null) final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? (widget.collapsedHeight ?? 0.0) + bottomHeight + topPadding ? (widget.collapsedHeight ?? 0.0) + bottomHeight + topPadding
: (widget.collapsedHeight ?? widget.toolbarHeight) + 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( return MediaQuery.removePadding(
context: context, context: context,
...@@ -2017,7 +1999,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -2017,7 +1999,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
automaticallyImplyLeading: widget.automaticallyImplyLeading, automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title, title: widget.title,
actions: widget.actions, actions: widget.actions,
flexibleSpace: widget.flexibleSpace, flexibleSpace: effectiveFlexibleSpace,
bottom: widget.bottom, bottom: widget.bottom,
elevation: widget.elevation, elevation: widget.elevation,
scrolledUnderElevation: widget.scrolledUnderElevation, scrolledUnderElevation: widget.scrolledUnderElevation,
...@@ -2032,8 +2014,8 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -2032,8 +2014,8 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
centerTitle: widget.centerTitle, centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics, excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing, titleSpacing: widget.titleSpacing,
expandedHeight: widget.expandedHeight, expandedHeight: effectiveExpandedHeight,
collapsedHeight: collapsedHeight, collapsedHeight: effectiveCollapsedHeight,
topPadding: topPadding, topPadding: topPadding,
floating: widget.floating, floating: widget.floating,
pinned: widget.pinned, pinned: widget.pinned,
...@@ -2048,6 +2030,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -2048,6 +2030,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
systemOverlayStyle: widget.systemOverlayStyle, systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency, forceMaterialTransparency: widget.forceMaterialTransparency,
clipBehavior: widget.clipBehavior, clipBehavior: widget.clipBehavior,
variant: widget._variant,
), ),
), ),
); );
...@@ -2098,35 +2081,30 @@ enum _ScrollUnderFlexibleVariant { medium, large } ...@@ -2098,35 +2081,30 @@ enum _ScrollUnderFlexibleVariant { medium, large }
class _ScrollUnderFlexibleSpace extends StatelessWidget { class _ScrollUnderFlexibleSpace extends StatelessWidget {
const _ScrollUnderFlexibleSpace({ const _ScrollUnderFlexibleSpace({
required this.hasLeading,
this.title, this.title,
this.actions,
this.foregroundColor, this.foregroundColor,
required this.variant, required this.variant,
this.centerCollapsedTitle,
this.primary = true, this.primary = true,
this.leadingWidth, this.titleTextStyle,
this.titleSpacing, required this.bottomHeight,
}); });
final bool hasLeading;
final Widget? title; final Widget? title;
final List<Widget>? actions;
final Color? foregroundColor; final Color? foregroundColor;
final _ScrollUnderFlexibleVariant variant; final _ScrollUnderFlexibleVariant variant;
final bool? centerCollapsedTitle;
final bool primary; final bool primary;
final double? leadingWidth; final TextStyle? titleTextStyle;
final double? titleSpacing; final double bottomHeight;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
late final ThemeData theme = Theme.of(context); late final ThemeData theme = Theme.of(context);
late final AppBarTheme appBarTheme = AppBarTheme.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 FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
final double topPadding = primary ? MediaQuery.viewPaddingOf(context).top : 0; final double topPadding = primary ? MediaQuery.viewPaddingOf(context).top : 0;
final double collapsedHeight = settings.minExtent - topPadding; final double collapsedHeight = settings.minExtent - topPadding - bottomHeight;
final double scrollUnderHeight = settings.maxExtent - settings.minExtent; final double scrollUnderHeight = settings.maxExtent - settings.minExtent + bottomHeight;
final _ScrollUnderFlexibleConfig config; final _ScrollUnderFlexibleConfig config;
switch (variant) { switch (variant) {
case _ScrollUnderFlexibleVariant.medium: case _ScrollUnderFlexibleVariant.medium:
...@@ -2135,71 +2113,46 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget { ...@@ -2135,71 +2113,46 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget {
config = _LargeScrollUnderFlexibleConfig(context); config = _LargeScrollUnderFlexibleConfig(context);
} }
late final Widget? collapsedTitle;
late final Widget? expandedTitle; late final Widget? expandedTitle;
if (title != null) { if (title != null) {
collapsedTitle = config.collapsedTextStyle != null final TextStyle? expandedTextStyle = titleTextStyle
? DefaultTextStyle( ?? appBarTheme.titleTextStyle
style: config.collapsedTextStyle!.copyWith(color: foregroundColor ?? appBarTheme.foregroundColor), ?? config.expandedTextStyle?.copyWith(
child: title!, color: foregroundColor ?? appBarTheme.foregroundColor ?? defaults.foregroundColor,
) );
: title;
expandedTitle = config.expandedTextStyle != null expandedTitle = config.expandedTextStyle != null
? DefaultTextStyle( ? DefaultTextStyle(
style: config.expandedTextStyle!.copyWith(color: foregroundColor ?? appBarTheme.foregroundColor), style: expandedTextStyle!,
child: title!, child: title!,
) )
: title; : title;
} }
late final bool centerTitle; EdgeInsetsGeometry expandedTitlePadding() {
{ final EdgeInsets padding = config.expandedTitlePadding!.resolve(Directionality.of(context));
bool platformCenter() { if (bottomHeight > 0) {
switch (theme.platform) { return EdgeInsets.fromLTRB(padding.left, 0, padding.right, bottomHeight);
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return false;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return true;
}
} }
centerTitle = centerCollapsedTitle ?? appBarTheme.centerTitle ?? platformCenter(); if (theme.useMaterial3) {
} final TextStyle textStyle = config.expandedTextStyle!;
// Substract the bottom line height from the bottom padding.
EdgeInsetsGeometry effectiveCollapsedTitlePadding = EdgeInsets.zero; // TODO(tahatesser): Figure out why this is done.
if (hasLeading && leadingWidth == null) { // https://github.com/flutter/flutter/issues/120672
effectiveCollapsedTitlePadding = centerTitle final double adjustBottomPadding = padding.bottom
? config.collapsedCenteredTitlePadding! - (textStyle.fontSize! * textStyle.height! - textStyle.fontSize!) / 2;
: config.collapsedTitlePadding!; return EdgeInsets.fromLTRB(
} else if (hasLeading && leadingWidth != null) { padding.left,
effectiveCollapsedTitlePadding = EdgeInsetsDirectional.only(start: leadingWidth!); 0,
padding.right,
adjustBottomPadding,
);
}
return padding;
} }
final bool isCollapsed = settings.isScrolledUnder ?? false;
return Column( return Column(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.only(top: topPadding), padding: EdgeInsets.only(top: collapsedHeight + 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,
),
),
), ),
Flexible( Flexible(
child: ClipRect( child: ClipRect(
...@@ -2209,7 +2162,7 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget { ...@@ -2209,7 +2162,7 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget {
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Container( child: Container(
alignment: AlignmentDirectional.bottomStart, alignment: AlignmentDirectional.bottomStart,
padding: config.expandedTitlePadding, padding: expandedTitlePadding(),
child: expandedTitle, child: expandedTitle,
), ),
), ),
...@@ -2223,8 +2176,6 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget { ...@@ -2223,8 +2176,6 @@ class _ScrollUnderFlexibleSpace extends StatelessWidget {
mixin _ScrollUnderFlexibleConfig { mixin _ScrollUnderFlexibleConfig {
TextStyle? get collapsedTextStyle; TextStyle? get collapsedTextStyle;
TextStyle? get expandedTextStyle; TextStyle? get expandedTextStyle;
EdgeInsetsGeometry? get collapsedTitlePadding;
EdgeInsetsGeometry? get collapsedCenteredTitlePadding;
EdgeInsetsGeometry? get expandedTitlePadding; EdgeInsetsGeometry? get expandedTitlePadding;
} }
...@@ -2332,12 +2283,6 @@ class _MediumScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig { ...@@ -2332,12 +2283,6 @@ class _MediumScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
TextStyle? get expandedTextStyle => TextStyle? get expandedTextStyle =>
_textTheme.headlineSmall?.apply(color: _colors.onSurface); _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 @override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 20); EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 20);
} }
...@@ -2361,12 +2306,6 @@ class _LargeScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig { ...@@ -2361,12 +2306,6 @@ class _LargeScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
TextStyle? get expandedTextStyle => TextStyle? get expandedTextStyle =>
_textTheme.headlineMedium?.apply(color: _colors.onSurface); _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 @override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 28); EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 28);
} }
......
...@@ -1086,16 +1086,18 @@ void main() { ...@@ -1086,16 +1086,18 @@ void main() {
}); });
testWidgets('SliverAppBar.medium defaults', (WidgetTester tester) async { testWidgets('SliverAppBar.medium defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const double collapsedAppBarHeight = 64; const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 112; const double expandedAppBarHeight = 112;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold( home: Scaffold(
body: CustomScrollView( body: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.medium( const SliverAppBar.medium(
title: const Text('AppBar Title'), title: Text('AppBar Title'),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
...@@ -1109,21 +1111,20 @@ void main() { ...@@ -1109,21 +1111,20 @@ void main() {
)); ));
final ScrollController controller = primaryScrollController(tester); final ScrollController controller = primaryScrollController(tester);
// There are two widgets for the title. The first is the title on the main // There are two widgets for the title. The first title is a larger version
// row with the icons. It is transparent when the app bar is expanded, and // that is shown at the bottom when the app bar is expanded. It scrolls under
// opaque when it is collapsed. The second title is a larger version that is // the main row until it is completely hidden and then the first title is
// shown at the bottom when the app bar is expanded. It scrolls under the // faded in. The last is the title on the mainrow with the icons. It is
// main row until it is completely hidden and then the first title is faded // transparent when the app bar is expanded, and opaque when it is collapsed.
// in. final Finder expandedTitle = find.text('AppBar Title').first;
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;
final Finder expandedTitleClip = find.ancestor( final Finder expandedTitleClip = find.ancestor(
of: expandedTitle, of: expandedTitle,
matching: find.byType(ClipRect), 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. // Default, fully expanded app bar.
...@@ -1133,6 +1134,17 @@ void main() { ...@@ -1133,6 +1134,17 @@ void main() {
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0); expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); 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. // Scroll the expanded app bar partially out of view.
controller.jumpTo(45); controller.jumpTo(45);
await tester.pump(); await tester.pump();
...@@ -1159,16 +1171,18 @@ void main() { ...@@ -1159,16 +1171,18 @@ void main() {
}); });
testWidgets('SliverAppBar.large defaults', (WidgetTester tester) async { testWidgets('SliverAppBar.large defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const double collapsedAppBarHeight = 64; const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 152; const double expandedAppBarHeight = 152;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold( home: Scaffold(
body: CustomScrollView( body: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.large( const SliverAppBar.large(
title: const Text('AppBar Title'), title: Text('AppBar Title'),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container( child: Container(
...@@ -1182,21 +1196,20 @@ void main() { ...@@ -1182,21 +1196,20 @@ void main() {
)); ));
final ScrollController controller = primaryScrollController(tester); final ScrollController controller = primaryScrollController(tester);
// There are two widgets for the title. The first is the title on the main // There are two widgets for the title. The first title is a larger version
// row with the icons. It is transparent when the app bar is expanded, and // that is shown at the bottom when the app bar is expanded. It scrolls under
// opaque when it is collapsed. The second title is a larger version that is // the main row until it is completely hidden and then the first title is
// shown at the bottom when the app bar is expanded. It scrolls under the // faded in. The last is the title on the mainrow with the icons. It is
// main row until it is completely hidden and then the first title is faded // transparent when the app bar is expanded, and opaque when it is collapsed.
// in. final Finder expandedTitle = find.text('AppBar Title').first;
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;
final Finder expandedTitleClip = find.ancestor( final Finder expandedTitleClip = find.ancestor(
of: expandedTitle, of: expandedTitle,
matching: find.byType(ClipRect), 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. // Default, fully expanded app bar.
...@@ -1206,6 +1219,19 @@ void main() { ...@@ -1206,6 +1219,19 @@ void main() {
expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0); expect(tester.widget<AnimatedOpacity>(collapsedTitleOpacity).opacity, 0);
expect(tester.getSize(expandedTitleClip).height, expandedAppBarHeight - collapsedAppBarHeight); 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. // Scroll the expanded app bar partially out of view.
controller.jumpTo(45); controller.jumpTo(45);
await tester.pump(); await tester.pump();
...@@ -4049,6 +4075,7 @@ void main() { ...@@ -4049,6 +4075,7 @@ void main() {
onPressed: () {}, onPressed: () {},
), ),
title: const Text(title, maxLines: 1), title: const Text(title, maxLines: 1),
centerTitle: true,
actions: const <Widget>[ actions: const <Widget>[
Icon(Icons.search), Icon(Icons.search),
Icon(Icons.sort), Icon(Icons.sort),
...@@ -4073,11 +4100,11 @@ void main() { ...@@ -4073,11 +4100,11 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu)); 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. // The title widget should be to the right of the leading widget.
expect(titleOffset.dx, greaterThan(leadingOffset.dx)); 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)); final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
// The title widget should be to the left of the search icon. // The title widget should be to the left of the search icon.
expect(titleOffset.dx, lessThan(searchOffset.dx)); expect(titleOffset.dx, lessThan(searchOffset.dx));
...@@ -4100,6 +4127,7 @@ void main() { ...@@ -4100,6 +4127,7 @@ void main() {
onPressed: () {}, onPressed: () {},
), ),
title: const Text(title, maxLines: 1), title: const Text(title, maxLines: 1),
centerTitle: true,
actions: const <Widget>[ actions: const <Widget>[
Icon(Icons.search), Icon(Icons.search),
Icon(Icons.sort), Icon(Icons.sort),
...@@ -4124,11 +4152,11 @@ void main() { ...@@ -4124,11 +4152,11 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu)); 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. // The title widget should be to the right of the leading widget.
expect(titleOffset.dx, greaterThan(leadingOffset.dx)); 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)); final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
// The title widget should be to the left of the search icon. // The title widget should be to the left of the search icon.
expect(titleOffset.dx, lessThan(searchOffset.dx)); expect(titleOffset.dx, lessThan(searchOffset.dx));
...@@ -4144,18 +4172,21 @@ void main() { ...@@ -4144,18 +4172,21 @@ void main() {
body: CustomScrollView( body: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.medium( SliverPadding(
centerTitle: centerTitle, padding: const EdgeInsets.symmetric(horizontal: 200),
leading: IconButton( sliver: SliverAppBar.medium(
icon: const Icon(Icons.menu), leading: IconButton(
onPressed: () {}, 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( SliverToBoxAdapter(
child: Container( child: Container(
...@@ -4171,55 +4202,57 @@ void main() { ...@@ -4171,55 +4202,57 @@ void main() {
await tester.pumpWidget(buildWidget()); await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(tester); ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// By default, title widget should be to the right of the // By default, title widget should be to the right of the
// leading widget and title spacing should be respected. // leading widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(find.text(title).first); Offset titleOffset = tester.getTopLeft(collapsedTitle);
Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); Offset iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing); expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);
await tester.pumpWidget(buildWidget(centerTitle: true)); await tester.pumpWidget(buildWidget(centerTitle: true));
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// By default, title widget should be to the left of the first // By default, title widget should be to the left of the first
// leading widget and title spacing should be respected. // trailing widget and title spacing should be respected.
titleOffset = tester.getTopRight(find.text(title).first); titleOffset = tester.getTopRight(collapsedTitle);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort)); iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconOffset.dx - titleSpacing); expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);
// Test custom title spacing, set to 0.0. // Test custom title spacing, set to 0.0.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0)); await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// The title widget should be to the right of the leading // The title widget should be to the right of the leading
// widget with no spacing. // widget with no spacing.
titleOffset = tester.getTopLeft(find.text(title).first); titleOffset = tester.getTopLeft(collapsedTitle);
iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx); expect(titleOffset.dx, iconButtonOffset.dx);
// Set centerTitle to true so the end of the title can reach // Set centerTitle to true so the end of the title can reach
// the action widgets. // the action widgets.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true)); await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// The title widget should be to the left of the first // The title widget should be to the left of the first
// leading widget with no spacing. // leading widget with no spacing.
titleOffset = tester.getTopRight(find.text(title).first); titleOffset = tester.getTopRight(collapsedTitle);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort)); iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconOffset.dx); expect(titleOffset.dx, iconButtonOffset.dx);
}); });
testWidgets('SliverAppBar.large respects title spacing', (WidgetTester tester) async { testWidgets('SliverAppBar.large respects title spacing', (WidgetTester tester) async {
...@@ -4232,18 +4265,21 @@ void main() { ...@@ -4232,18 +4265,21 @@ void main() {
body: CustomScrollView( body: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.large( SliverPadding(
centerTitle: centerTitle, padding: const EdgeInsets.symmetric(horizontal: 200),
leading: IconButton( sliver: SliverAppBar.large(
icon: const Icon(Icons.menu), leading: IconButton(
onPressed: () {}, 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( SliverToBoxAdapter(
child: Container( child: Container(
...@@ -4259,61 +4295,63 @@ void main() { ...@@ -4259,61 +4295,63 @@ void main() {
await tester.pumpWidget(buildWidget()); await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(tester); ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(160);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// By default, title widget should be to the right of the leading // By default, title widget should be to the right of the leading
// widget and title spacing should be respected. // widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(find.text(title).first); Offset titleOffset = tester.getTopLeft(collapsedTitle);
Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); Offset iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing); expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);
await tester.pumpWidget(buildWidget(centerTitle: true)); await tester.pumpWidget(buildWidget(centerTitle: true));
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(160);
await tester.pumpAndSettle(); 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. // leading widget and title spacing should be respected.
titleOffset = tester.getTopRight(find.text(title).first); titleOffset = tester.getTopRight(collapsedTitle);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort)); iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconOffset.dx - titleSpacing); expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);
// Test custom title spacing, set to 0.0. // Test custom title spacing, set to 0.0.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0)); await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
controller = primaryScrollController(tester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(160);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// The title widget should be to the right of the leading // The title widget should be to the right of the leading
// widget with no spacing. // widget with no spacing.
titleOffset = tester.getTopLeft(find.text(title).first); titleOffset = tester.getTopLeft(collapsedTitle);
iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); iconButtonOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx); expect(titleOffset.dx, iconButtonOffset.dx);
// Set centerTitle to true so the end of the title can reach // Set centerTitle to true so the end of the title can reach
// the action widgets. // the action widgets.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true)); await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(160);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// The title widget should be to the left of the first // The title widget should be to the left of the first
// leading widget with no spacing. // leading widget with no spacing.
titleOffset = tester.getTopRight(find.text(title).first); titleOffset = tester.getTopRight(collapsedTitle);
iconOffset = tester.getTopLeft(find.byIcon(Icons.sort)); iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconOffset.dx); expect(titleOffset.dx, iconButtonOffset.dx);
}); });
testWidgets( testWidgets(
'SliverAppBar.medium without the leading widget updates collapsed title padding', 'SliverAppBar.medium without the leading widget updates collapsed title padding',
(WidgetTester widgetTester) async { (WidgetTester tester) async {
const String title = 'Medium SliverAppBar Title'; const String title = 'Medium SliverAppBar Title';
const double leadingPadding = 40.0; const double leadingPadding = 56.0;
const double titleSpacing = 16.0; const double titleSpacing = 16.0;
Widget buildWidget({ bool showLeading = true }) { Widget buildWidget({ bool showLeading = true }) {
...@@ -4323,6 +4361,7 @@ void main() { ...@@ -4323,6 +4361,7 @@ void main() {
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.medium( SliverAppBar.medium(
automaticallyImplyLeading: false,
leading: showLeading leading: showLeading
? IconButton( ? IconButton(
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
...@@ -4343,36 +4382,38 @@ void main() { ...@@ -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. // Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(widgetTester); ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(45);
await widgetTester.pumpAndSettle(); await tester.pumpAndSettle();
// If the leading widget is present, the title widget should be to the // If the leading widget is present, the title widget should be to the
// right of the leading widget and title spacing should be respected. // 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); expect(titleOffset.dx, leadingPadding + titleSpacing);
// Hide the leading widget. // Hide the leading widget.
await widgetTester.pumpWidget(buildWidget(showLeading: false)); await tester.pumpWidget(buildWidget(showLeading: false));
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
controller = primaryScrollController(widgetTester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(45);
await widgetTester.pumpAndSettle(); await tester.pumpAndSettle();
// If the leading widget is not present, the title widget will // If the leading widget is not present, the title widget will
// only have the default title spacing. // only have the default title spacing.
titleOffset = widgetTester.getTopLeft(find.text(title).first); titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, titleSpacing); expect(titleOffset.dx, titleSpacing);
}); });
testWidgets( testWidgets(
'SliverAppBar.large without the leading widget updates collapsed title padding', 'SliverAppBar.large without the leading widget updates collapsed title padding',
(WidgetTester widgetTester) async { (WidgetTester tester) async {
const String title = 'Large SliverAppBar Title'; const String title = 'Large SliverAppBar Title';
const double leadingPadding = 40.0; const double leadingPadding = 56.0;
const double titleSpacing = 16.0; const double titleSpacing = 16.0;
Widget buildWidget({ bool showLeading = true }) { Widget buildWidget({ bool showLeading = true }) {
...@@ -4382,6 +4423,7 @@ void main() { ...@@ -4382,6 +4423,7 @@ void main() {
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.large( SliverAppBar.large(
automaticallyImplyLeading: false,
leading: showLeading leading: showLeading
? IconButton( ? IconButton(
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
...@@ -4402,31 +4444,211 @@ void main() { ...@@ -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. // Scroll CustomScrollView to collapse SliverAppBar.
ScrollController controller = primaryScrollController(widgetTester); ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(45);
await widgetTester.pumpAndSettle(); await tester.pumpAndSettle();
// If the leading widget is present, the title widget should be to the // If the leading widget is present, the title widget should be to the
// right of the leading widget and title spacing should be respected. // 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); expect(titleOffset.dx, leadingPadding + titleSpacing);
// Hide the leading widget. // Hide the leading widget.
await widgetTester.pumpWidget(buildWidget(showLeading: false)); await tester.pumpWidget(buildWidget(showLeading: false));
// Scroll to collapse the SliverAppBar. // Scroll to collapse the SliverAppBar.
controller = primaryScrollController(widgetTester); controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(45);
await widgetTester.pumpAndSettle(); await tester.pumpAndSettle();
// If the leading widget is not present, the title widget will // If the leading widget is not present, the title widget will
// only have the default title spacing. // only have the default title spacing.
titleOffset = widgetTester.getTopLeft(find.text(title).first); titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, titleSpacing); 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', () { group('AppBar.forceMaterialTransparency', () {
Material getAppBarMaterial(WidgetTester tester) { Material getAppBarMaterial(WidgetTester tester) {
return tester.widget<Material>(find return tester.widget<Material>(find
...@@ -4501,4 +4723,177 @@ void main() { ...@@ -4501,4 +4723,177 @@ void main() {
expect(buttonWasPressed, isFalse); 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'; ...@@ -9,6 +9,25 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { 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) { ScrollController primaryScrollController(WidgetTester tester) {
return PrimaryScrollController.of(tester.element(find.byType(CustomScrollView))); return PrimaryScrollController.of(tester.element(find.byType(CustomScrollView)));
} }
...@@ -681,18 +700,10 @@ void main() { ...@@ -681,18 +700,10 @@ void main() {
}); });
testWidgets('SliverAppBar.medium uses AppBarTheme properties', (WidgetTester tester) async { testWidgets('SliverAppBar.medium uses AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Medium SliverAppBar Title'; const String title = 'Medium App Bar';
const Color foregroundColor = Color(0xff00ff00);
const double titleSpacing = 10.0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: ThemeData( theme: ThemeData(appBarTheme: appBarTheme),
appBarTheme: const AppBarTheme(
foregroundColor: foregroundColor,
titleSpacing: titleSpacing,
centerTitle: false,
),
),
home: CustomScrollView( home: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
...@@ -702,82 +713,130 @@ void main() { ...@@ -702,82 +713,130 @@ void main() {
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
), ),
title: const Text(title), title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
), ),
], ],
), ),
)); ));
final RichText text = tester.firstWidget(find.byType(RichText)); // Test title.
expect(text.text.style!.color, foregroundColor); 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. // Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester); final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first); // Test title spacing.
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); final Finder collapsedTitle = find.text(title).last;
// Title spacing should be 10.0. final Offset titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, iconOffset.dx + titleSpacing); 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 { testWidgets('SliverAppBar.medium properties take priority over AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Medium SliverAppBar Title'; const String title = 'Medium App Bar';
const Color foregroundColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff000099);
const double titleSpacing = 10.0; 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( await tester.pumpWidget(MaterialApp(
theme: ThemeData( theme: ThemeData(appBarTheme: appBarTheme),
appBarTheme: const AppBarTheme(
foregroundColor: Color(0xffff0000),
titleSpacing: 14.0,
centerTitle: true,
),
),
home: CustomScrollView( home: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.medium( SliverAppBar.medium(
centerTitle: false, centerTitle: false,
titleSpacing: titleSpacing, backgroundColor: backgroundColor,
foregroundColor: foregroundColor, foregroundColor: foregroundColor,
shadowColor: shadowColor,
shape: shape,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
titleSpacing: titleSpacing,
titleTextStyle: titleTextStyle,
leading: IconButton( leading: IconButton(
onPressed: () {}, onPressed: () {},
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
), ),
title: const Text(title), title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
), ),
], ],
), ),
)); ));
final RichText text = tester.firstWidget(find.byType(RichText)); // Test title.
expect(text.text.style!.color, foregroundColor); 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. // Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester); final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first); // Test title spacing.
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); final Finder collapsedTitle = find.text(title).last;
// Title spacing should be 10.0. final Offset titleOffset = tester.getTopLeft(collapsedTitle);
final Offset iconOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing); expect(titleOffset.dx, iconOffset.dx + titleSpacing);
}); });
testWidgets('SliverAppBar.large uses AppBarTheme properties', (WidgetTester tester) async { testWidgets('SliverAppBar.large uses AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Large SliverAppBar Title'; const String title = 'Large App Bar';
const Color foregroundColor = Color(0xff00ff00);
const double titleSpacing = 10.0;
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
theme: ThemeData( theme: ThemeData(appBarTheme: appBarTheme),
appBarTheme: const AppBarTheme(
foregroundColor: foregroundColor,
titleSpacing: titleSpacing,
centerTitle: false,
),
),
home: CustomScrollView( home: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
...@@ -787,69 +846,169 @@ void main() { ...@@ -787,69 +846,169 @@ void main() {
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
), ),
title: const Text(title), title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
), ),
], ],
), ),
)); ));
final RichText text = tester.firstWidget(find.byType(RichText)); // Test title.
expect(text.text.style!.color, foregroundColor); 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. // Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester); final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first); // Test title spacing.
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); final Finder collapsedTitle = find.text(title).last;
// Title spacing should be 10.0. final Offset titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, iconOffset.dx + titleSpacing); 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 { testWidgets('SliverAppBar.large properties take priority over AppBarTheme properties', (WidgetTester tester) async {
const String title = 'Large SliverAppBar Title'; const String title = 'Large App Bar';
const Color foregroundColor = Color(0xff00ff00); const Color backgroundColor = Color(0xff000099);
const double titleSpacing = 10.0; 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( await tester.pumpWidget(MaterialApp(
theme: ThemeData( theme: ThemeData(appBarTheme: appBarTheme),
appBarTheme: const AppBarTheme(
foregroundColor: Color(0xffff0000),
titleSpacing: 14.0,
centerTitle: true,
),
),
home: CustomScrollView( home: CustomScrollView(
primary: true, primary: true,
slivers: <Widget>[ slivers: <Widget>[
SliverAppBar.large( SliverAppBar.large(
centerTitle: false, centerTitle: false,
titleSpacing: titleSpacing, backgroundColor: backgroundColor,
foregroundColor: foregroundColor, foregroundColor: foregroundColor,
shadowColor: shadowColor,
shape: shape,
iconTheme: iconTheme,
actionsIconTheme: actionsIconTheme,
titleSpacing: titleSpacing,
titleTextStyle: titleTextStyle,
leading: IconButton( leading: IconButton(
onPressed: () {}, onPressed: () {},
icon: const Icon(Icons.menu), icon: const Icon(Icons.menu),
), ),
title: const Text(title), title: const Text(title),
actions: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.search),
),
],
), ),
], ],
), ),
)); ));
final RichText text = tester.firstWidget(find.byType(RichText)); // Test title.
expect(text.text.style!.color, foregroundColor); 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. // Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester); final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45); controller.jumpTo(120);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final Offset titleOffset = tester.getTopLeft(find.text(title).first); // Test title spacing.
final Offset iconOffset = tester.getTopRight(find.byIcon(Icons.menu)); final Finder collapsedTitle = find.text(title).last;
// Title spacing should be 10.0. final Offset titleOffset = tester.getTopLeft(collapsedTitle);
final Offset iconOffset = tester.getTopRight(find.widgetWithIcon(IconButton, Icons.menu));
expect(titleOffset.dx, iconOffset.dx + titleSpacing); 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 { testWidgets('Default AppBarTheme debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const AppBarTheme().debugFillProperties(builder); const AppBarTheme().debugFillProperties(builder);
......
...@@ -2720,8 +2720,8 @@ void main() { ...@@ -2720,8 +2720,8 @@ void main() {
return <Widget>[ return <Widget>[
SliverOverlapAbsorber( SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar.medium( sliver: const SliverAppBar.medium(
title: const Text('AppBar Title'), title: Text('AppBar Title'),
), ),
), ),
]; ];
...@@ -2747,11 +2747,11 @@ void main() { ...@@ -2747,11 +2747,11 @@ void main() {
)); ));
// There are two widgets for the title. // 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( final Finder expandedTitleClip = find.ancestor(
of: expandedTitle, of: expandedTitle,
matching: find.byType(ClipRect), matching: find.byType(ClipRect),
); ).first;
// Default, fully expanded app bar. // Default, fully expanded app bar.
expect(nestedScrollView.currentState?.outerController.offset, 0); expect(nestedScrollView.currentState?.outerController.offset, 0);
...@@ -2830,11 +2830,11 @@ void main() { ...@@ -2830,11 +2830,11 @@ void main() {
)); ));
// There are two widgets for the title. // 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( final Finder expandedTitleClip = find.ancestor(
of: expandedTitle, of: expandedTitle,
matching: find.byType(ClipRect), matching: find.byType(ClipRect),
); ).first;
// Default, fully expanded app bar. // Default, fully expanded app bar.
expect(nestedScrollView.currentState?.outerController.offset, 0); 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