Add support for Material 3 medium and large top app bars. (#103962)

* Add support for M3 AppBar 'Medium' and 'Large' types.

* Updates from review feedback.

* Updated from review feedback.
This commit is contained in:
Darren Austin 2022-05-20 14:02:25 -07:00 committed by GitHub
parent 7eed12075b
commit b08b88ce6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 708 additions and 4 deletions

View File

@ -54,5 +54,65 @@ class _TokenDefaultsM3 extends AppBarTheme {
@override
TextStyle? get titleTextStyle => ${textStyle('md.comp.top-app-bar.small.headline')};
}''';
}
// Variant configuration
class _MediumScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
_MediumScrollUnderFlexibleConfig(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
late final TextTheme _textTheme = _theme.textTheme;
static const double collapsedHeight = ${tokens['md.comp.top-app-bar.small.container.height']};
static const double expandedHeight = ${tokens['md.comp.top-app-bar.medium.container.height']};
@override
TextStyle? get collapsedTextStyle =>
${textStyle('md.comp.top-app-bar.small.headline')}?.apply(color: ${color('md.comp.top-app-bar.small.headline.color')});
@override
TextStyle? get expandedTextStyle =>
${textStyle('md.comp.top-app-bar.medium.headline')}?.apply(color: ${color('md.comp.top-app-bar.medium.headline.color')});
@override
EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
@override
EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 20);
}
class _LargeScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
_LargeScrollUnderFlexibleConfig(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
late final TextTheme _textTheme = _theme.textTheme;
static const double collapsedHeight = ${tokens['md.comp.top-app-bar.small.container.height']};
static const double expandedHeight = ${tokens['md.comp.top-app-bar.large.container.height']};
@override
TextStyle? get collapsedTextStyle =>
${textStyle('md.comp.top-app-bar.small.headline')}?.apply(color: ${color('md.comp.top-app-bar.small.headline.color')});
@override
TextStyle? get expandedTextStyle =>
${textStyle('md.comp.top-app-bar.large.headline')}?.apply(color: ${color('md.comp.top-app-bar.large.headline.color')});
@override
EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
@override
EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 28);
}
''';
}

View File

@ -0,0 +1,53 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for SliverAppBar.medium
import 'package:flutter/material.dart';
void main() {
runApp(const AppBarMediumApp());
}
class AppBarMediumApp extends StatelessWidget {
const AppBarMediumApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xff6750A4)
),
home: Material(
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar.medium(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
title: const Text('Medium App Bar'),
actions: <Widget>[
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
],
),
// Just some content big enough to have something to scroll.
SliverToBoxAdapter(
child: Card(
child: SizedBox(
height: 1200,
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 100, 8, 100),
child: Text(
'Here be scrolling content...',
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,53 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flutter code sample for SliverAppBar.large
import 'package:flutter/material.dart';
void main() {
runApp(const AppBarLargeApp());
}
class AppBarLargeApp extends StatelessWidget {
const AppBarLargeApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xff6750A4)
),
home: Material(
child: CustomScrollView(
slivers: <Widget>[
SliverAppBar.large(
leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}),
title: const Text('Large App Bar'),
actions: <Widget>[
IconButton(icon: const Icon(Icons.more_vert), onPressed: () {}),
],
),
// Just some content big enough to have something to scroll.
SliverToBoxAdapter(
child: Card(
child: SizedBox(
height: 1200,
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 100, 8, 100),
child: Text(
'Here be scrolling content...',
style: Theme.of(context).textTheme.headlineSmall,
),
),
),
),
),
],
),
),
);
}
}

View File

@ -860,8 +860,11 @@ class _AppBarState extends State<AppBar> {
?? MaterialStateProperty.resolveAs<Color>(defaultColor, states);
}
SystemUiOverlayStyle _systemOverlayStyleForBrightness(Brightness brightness) {
return brightness == Brightness.dark ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark;
SystemUiOverlayStyle _systemOverlayStyleForBrightness(Brightness brightness, [Color? backgroundColor]) {
final SystemUiOverlayStyle style = brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark;
return style.copyWith(statusBarColor: backgroundColor);
}
@override
@ -1139,7 +1142,12 @@ class _AppBarState extends State<AppBar> {
: widget.systemOverlayStyle
?? appBarTheme.systemOverlayStyle
?? defaults.systemOverlayStyle
?? _systemOverlayStyleForBrightness(ThemeData.estimateBrightnessForColor(backgroundColor));
?? _systemOverlayStyleForBrightness(
ThemeData.estimateBrightnessForColor(backgroundColor),
// Make the status bar transparent for M3 so the elevation overlay
// color is picked up by the statusbar.
theme.useMaterial3 ? const Color(0x00000000) : null,
);
return Semantics(
container: true,
@ -1517,6 +1525,208 @@ class SliverAppBar extends StatefulWidget {
assert(stretchTriggerOffset > 0.0),
assert(collapsedHeight == null || collapsedHeight >= toolbarHeight, 'The "collapsedHeight" argument has to be larger than or equal to [toolbarHeight].');
/// Creates a Material Design medium top app bar that can be placed
/// in a [CustomScrollView].
///
/// Returns a [SliverAppBar] configured with appropriate defaults
/// for a medium top app bar as defined in Material 3. It starts fully
/// expanded with the title in an area underneath the main row of icons.
/// When the [CustomScrollView] is scrolled, the title will be scrolled
/// under the main row. When it is fully collapsed, a smaller version of the
/// title will fade in on the main row. The reverse will happen if it is
/// expanded again.
///
/// {@tool dartpad}
/// This sample shows how to use [SliverAppBar.medium] in a [CustomScrollView].
///
/// ** See code in examples/api/lib/material/app_bar/sliver_app_bar.2.dart **
/// {@end-tool}
///
/// See also:
///
/// * [AppBar], for a small or center-aligned top app bar.
/// * [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,
actions: actions,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
title: title,
variant: _ScrollUnderFlexibleVariant.medium,
centerCollapsedTitle: centerTitle,
primary: primary,
),
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
/// in a [CustomScrollView].
///
/// Returns a [SliverAppBar] configured with appropriate defaults
/// for a large top app bar as defined in Material 3. It starts fully
/// expanded with the title in an area underneath the main row of icons.
/// When the [CustomScrollView] is scrolled, the title will be scrolled
/// under the main row. When it is fully collapsed, a smaller version of the
/// title will fade in on the main row. The reverse will happen if it is
/// expanded again.
///
/// {@tool dartpad}
/// This sample shows how to use [SliverAppBar.large] in a [CustomScrollView].
///
/// ** See code in examples/api/lib/material/app_bar/sliver_app_bar.3.dart **
/// {@end-tool}
///
/// See also:
///
/// * [AppBar], for a small or center-aligned top app bar.
/// * [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,
actions: actions,
flexibleSpace: flexibleSpace ?? _ScrollUnderFlexibleSpace(
title: title,
variant: _ScrollUnderFlexibleVariant.large,
centerCollapsedTitle: centerTitle,
primary: primary,
),
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}
///
/// This property is used to configure an [AppBar].
@ -1943,6 +2153,128 @@ class _RenderAppBarTitleBox extends RenderAligningShiftedBox {
}
}
enum _ScrollUnderFlexibleVariant { medium, large }
class _ScrollUnderFlexibleSpace extends StatefulWidget {
const _ScrollUnderFlexibleSpace({
this.title,
required this.variant,
this.centerCollapsedTitle,
this.primary = true,
});
final Widget? title;
final _ScrollUnderFlexibleVariant variant;
final bool? centerCollapsedTitle;
final bool primary;
@override
State<_ScrollUnderFlexibleSpace> createState() => _ScrollUnderFlexibleSpaceState();
}
class _ScrollUnderFlexibleSpaceState extends State<_ScrollUnderFlexibleSpace> {
@override
Widget build(BuildContext context) {
late final ThemeData theme = Theme.of(context);
final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
final double topPadding = widget.primary ? MediaQuery.of(context).viewPadding.top : 0;
final double collapsedHeight = settings.minExtent - topPadding;
final double scrollUnderHeight = settings.maxExtent - settings.minExtent;
final _ScrollUnderFlexibleConfig config;
switch (widget.variant) {
case _ScrollUnderFlexibleVariant.medium:
config = _MediumScrollUnderFlexibleConfig(context);
break;
case _ScrollUnderFlexibleVariant.large:
config = _LargeScrollUnderFlexibleConfig(context);
break;
}
late final Widget? collapsedTitle;
late final Widget? expandedTitle;
if (widget.title != null) {
collapsedTitle = config.collapsedTextStyle != null
? DefaultTextStyle(
style: config.collapsedTextStyle!,
child: widget.title!,
)
: widget.title;
expandedTitle = config.expandedTextStyle != null
? DefaultTextStyle(
style: config.expandedTextStyle!,
child: widget.title!,
)
: widget.title;
}
late final bool centerTitle;
{
bool platformCenter() {
assert(theme.platform != null);
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;
}
}
centerTitle = widget.centerCollapsedTitle
?? theme.appBarTheme.centerTitle
?? platformCenter();
}
final bool isCollapsed = settings.isScrolledUnder ?? false;
return Column(
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: topPadding),
child: Container(
height: collapsedHeight,
padding: centerTitle ? config.collapsedCenteredTitlePadding : config.collapsedTitlePadding,
child: AnimatedOpacity(
opacity: isCollapsed ? 1 : 0,
duration: const Duration(milliseconds: 500),
curve: const Cubic(0.2, 0.0, 0.0, 1.0),
child: Align(
alignment: centerTitle
? Alignment.center
: AlignmentDirectional.centerStart,
child: collapsedTitle
),
),
),
),
Flexible(
child: ClipRect(
child: OverflowBox(
minHeight: scrollUnderHeight,
maxHeight: scrollUnderHeight,
alignment: Alignment.bottomLeft,
child: Container(
alignment: AlignmentDirectional.bottomStart,
padding: config.expandedTitlePadding,
child: expandedTitle,
),
),
),
),
],
);
}
}
mixin _ScrollUnderFlexibleConfig {
TextStyle? get collapsedTextStyle;
TextStyle? get expandedTextStyle;
EdgeInsetsGeometry? get collapsedTitlePadding;
EdgeInsetsGeometry? get collapsedCenteredTitlePadding;
EdgeInsetsGeometry? get expandedTitlePadding;
}
class _DefaultsM2 extends AppBarTheme {
_DefaultsM2(this.context)
: super(
@ -2020,4 +2352,64 @@ class _TokenDefaultsM3 extends AppBarTheme {
@override
TextStyle? get titleTextStyle => _textTheme.titleLarge;
}
// Variant configuration
class _MediumScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
_MediumScrollUnderFlexibleConfig(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
late final TextTheme _textTheme = _theme.textTheme;
static const double collapsedHeight = 64.0;
static const double expandedHeight = 112.0;
@override
TextStyle? get collapsedTextStyle =>
_textTheme.titleLarge?.apply(color: _colors.onSurface);
@override
TextStyle? get expandedTextStyle =>
_textTheme.headlineSmall?.apply(color: _colors.onSurface);
@override
EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
@override
EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 20);
}
class _LargeScrollUnderFlexibleConfig with _ScrollUnderFlexibleConfig {
_LargeScrollUnderFlexibleConfig(this.context);
final BuildContext context;
late final ThemeData _theme = Theme.of(context);
late final ColorScheme _colors = _theme.colorScheme;
late final TextTheme _textTheme = _theme.textTheme;
static const double collapsedHeight = 64.0;
static const double expandedHeight = 152.0;
@override
TextStyle? get collapsedTextStyle =>
_textTheme.titleLarge?.apply(color: _colors.onSurface);
@override
TextStyle? get expandedTextStyle =>
_textTheme.headlineMedium?.apply(color: _colors.onSurface);
@override
EdgeInsetsGeometry? get collapsedTitlePadding => const EdgeInsetsDirectional.fromSTEB(48, 0, 16, 0);
@override
EdgeInsetsGeometry? get collapsedCenteredTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 0);
@override
EdgeInsetsGeometry? get expandedTitlePadding => const EdgeInsets.fromLTRB(16, 0, 16, 28);
}
// END GENERATED TOKEN PROPERTIES

View File

@ -953,6 +953,152 @@ void main() {
expect(tabBarHeight(tester), initialTabBarHeight);
});
testWidgets('SliverAppBar.medium defaults', (WidgetTester tester) async {
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 112;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
title: const 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 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;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
);
// 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);
// 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 {
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 152;
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.large(
title: const 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 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;
final Finder expandedTitleClip = find.ancestor(
of: expandedTitle,
matching: find.byType(ClipRect),
);
// 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);
// 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('AppBar uses the specified elevation or defaults to 4.0', (WidgetTester tester) async {
final bool useMaterial3 = ThemeData().useMaterial3;