From b08b88ce6cab4a10544045662477b644cc4f1fd6 Mon Sep 17 00:00:00 2001 From: Darren Austin Date: Fri, 20 May 2022 14:02:25 -0700 Subject: [PATCH] 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. --- .../gen_defaults/lib/app_bar_template.dart | 62 ++- .../material/app_bar/sliver_app_bar.2.dart | 53 +++ .../material/app_bar/sliver_app_bar.3.dart | 53 +++ .../flutter/lib/src/material/app_bar.dart | 398 +++++++++++++++++- .../flutter/test/material/app_bar_test.dart | 146 +++++++ 5 files changed, 708 insertions(+), 4 deletions(-) create mode 100644 examples/api/lib/material/app_bar/sliver_app_bar.2.dart create mode 100644 examples/api/lib/material/app_bar/sliver_app_bar.3.dart diff --git a/dev/tools/gen_defaults/lib/app_bar_template.dart b/dev/tools/gen_defaults/lib/app_bar_template.dart index 06b2ddfe028c..af8ac620c5bd 100644 --- a/dev/tools/gen_defaults/lib/app_bar_template.dart +++ b/dev/tools/gen_defaults/lib/app_bar_template.dart @@ -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); +} +'''; } diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.2.dart b/examples/api/lib/material/app_bar/sliver_app_bar.2.dart new file mode 100644 index 000000000000..77c344da76f7 --- /dev/null +++ b/examples/api/lib/material/app_bar/sliver_app_bar.2.dart @@ -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: [ + SliverAppBar.medium( + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + title: const Text('Medium App Bar'), + actions: [ + 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, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/examples/api/lib/material/app_bar/sliver_app_bar.3.dart b/examples/api/lib/material/app_bar/sliver_app_bar.3.dart new file mode 100644 index 000000000000..3c32a47d4e9c --- /dev/null +++ b/examples/api/lib/material/app_bar/sliver_app_bar.3.dart @@ -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: [ + SliverAppBar.large( + leading: IconButton(icon: const Icon(Icons.menu), onPressed: () {}), + title: const Text('Large App Bar'), + actions: [ + 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, + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 06f97af3255d..636a11879982 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -860,8 +860,11 @@ class _AppBarState extends State { ?? MaterialStateProperty.resolveAs(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 { : 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? 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? 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()!; + 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: [ + 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 diff --git a/packages/flutter/test/material/app_bar_test.dart b/packages/flutter/test/material/app_bar_test.dart index 1aec5d453934..824a711c415b 100644 --- a/packages/flutter/test/material/app_bar_test.dart +++ b/packages/flutter/test/material/app_bar_test.dart @@ -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: [ + 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(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(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(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(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: [ + 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(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(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(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(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;