blob: cef0f0f5bf2a127a1276afc87ca49704f149b424 [file] [log] [blame]
// 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.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
import 'app_bar_utils.dart';
Widget buildSliverAppBarApp({
bool floating = false,
bool pinned = false,
double? collapsedHeight,
double? expandedHeight,
bool snap = false,
double toolbarHeight = kToolbarHeight,
}) {
return MaterialApp(
home: Scaffold(
body: DefaultTabController(
length: 3,
child: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar(
title: const Text('AppBar Title'),
floating: floating,
pinned: pinned,
collapsedHeight: collapsedHeight,
expandedHeight: expandedHeight,
toolbarHeight: toolbarHeight,
snap: snap,
bottom: TabBar(
tabs: <String>['A','B','C'].map<Widget>((String t) => Tab(text: 'TAB $t')).toList(),
),
),
SliverToBoxAdapter(
child: Container(
height: 1200.0,
color: Colors.orange[400],
),
),
],
),
),
),
);
}
void main() {
setUp(() {
debugResetSemanticsIdCounter();
});
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);
});
testWidgets('SliverAppBar.medium expanded title has upper limit on text scaling', (WidgetTester tester) async {
const String title = 'Medium AppBar';
Widget buildAppBar({double textScaleFactor = 1.0}) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: MediaQuery.withClampedTextScaling(
minScaleFactor: textScaleFactor,
maxScaleFactor: textScaleFactor,
child: Material(
child: CustomScrollView(
slivers: <Widget>[
const SliverAppBar.medium(
title: Text(title),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
),
);
}
await tester.pumpWidget(buildAppBar());
final Finder expandedTitle = find.text(title).first;
expect(tester.getRect(expandedTitle).height, 32.0);
verifyTextNotClipped(expandedTitle, tester);
await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
expect(tester.getRect(expandedTitle).height, 43.0);
verifyTextNotClipped(expandedTitle, tester);
await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
expect(tester.getRect(expandedTitle).height, 43.0);
verifyTextNotClipped(expandedTitle, tester);
}, skip: kIsWeb && !isSkiaWeb); // https://github.com/flutter/flutter/issues/99933
testWidgets('SliverAppBar.large expanded title has upper limit on text scaling', (WidgetTester tester) async {
const String title = 'Large AppBar';
Widget buildAppBar({double textScaleFactor = 1.0}) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: MediaQuery.withClampedTextScaling(
minScaleFactor: textScaleFactor,
maxScaleFactor: textScaleFactor,
child: Material(
child: CustomScrollView(
slivers: <Widget>[
const SliverAppBar.large(
title: Text(title, maxLines: 1),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
),
);
}
await tester.pumpWidget(buildAppBar());
final Finder expandedTitle = find.text(title).first;
expect(tester.getRect(expandedTitle).height, 36.0);
await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
expect(tester.getRect(expandedTitle).height, closeTo(48.0, 0.1));
await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
expect(tester.getRect(expandedTitle).height, closeTo(48.0, 0.1));
}, skip: kIsWeb && !isSkiaWeb); // https://github.com/flutter/flutter/issues/99933
testWidgets('SliverAppBar.medium expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async {
const String title = 'Medium AppBar';
Widget buildAppBar({double textScaleFactor = 1.0}) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: MediaQuery.withClampedTextScaling(
minScaleFactor: textScaleFactor,
maxScaleFactor: textScaleFactor,
child: Material(
child: CustomScrollView(
slivers: <Widget>[
const SliverAppBar.medium(
title: Text(title, maxLines: 1),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
),
);
}
await tester.pumpWidget(buildAppBar());
final Finder expandedTitle = find.text(title).first;
expect(tester.getBottomLeft(expandedTitle).dy, 96.0);
verifyTextNotClipped(expandedTitle, tester);
await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
expect(tester.getBottomLeft(expandedTitle).dy, 107.0);
verifyTextNotClipped(expandedTitle, tester);
await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
expect(tester.getBottomLeft(expandedTitle).dy, 107.0);
verifyTextNotClipped(expandedTitle, tester);
}, skip: kIsWeb && !isSkiaWeb); // https://github.com/flutter/flutter/issues/99933
testWidgets('SliverAppBar.large expanded title position is adjusted with textScaleFactor', (WidgetTester tester) async {
const String title = 'Large AppBar';
Widget buildAppBar({double textScaleFactor = 1.0}) {
return MaterialApp(
theme: ThemeData(useMaterial3: true),
home: MediaQuery.withClampedTextScaling(
minScaleFactor: textScaleFactor,
maxScaleFactor: textScaleFactor,
child: Material(
child: CustomScrollView(
slivers: <Widget>[
const SliverAppBar.large(
title: Text(title, maxLines: 1),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
),
);
}
await tester.pumpWidget(buildAppBar());
final Finder expandedTitle = find.text(title).first;
final RenderSliver renderSliverAppBar = tester.renderObject(find.byType(SliverAppBar));
expect(
tester.getBottomLeft(expandedTitle).dy,
renderSliverAppBar.geometry!.scrollExtent - 28.0,
reason: 'bottom padding of a large expanded title should be 28.',
);
verifyTextNotClipped(expandedTitle, tester);
await tester.pumpWidget(buildAppBar(textScaleFactor: 2.0));
expect(
tester.getBottomLeft(expandedTitle).dy,
renderSliverAppBar.geometry!.scrollExtent - 28.0,
reason: 'bottom padding of a large expanded title should be 28.',
);
verifyTextNotClipped(expandedTitle, tester);
// The bottom padding of the expanded title needs to be reduced for it to be
// fully visible.
await tester.pumpWidget(buildAppBar(textScaleFactor: 3.0));
expect(tester.getBottomLeft(expandedTitle).dy, 124.0);
verifyTextNotClipped(expandedTitle, tester);
});
testWidgets(
'SliverAppBar.medium collapsed title does not overlap with leading/actions widgets',
(WidgetTester tester) async {
const String title = 'Medium SliverAppBar Very Long Title';
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 200),
sliver: SliverAppBar.medium(
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
),
title: const Text(title, maxLines: 1),
centerTitle: true,
actions: const <Widget>[
Icon(Icons.search),
Icon(Icons.sort),
Icon(Icons.more_vert),
],
),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
));
// Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
await tester.pumpAndSettle();
final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu));
Offset titleOffset = tester.getTopLeft(find.text(title).last);
// The title widget should be to the right of the leading widget.
expect(titleOffset.dx, greaterThan(leadingOffset.dx));
titleOffset = tester.getTopRight(find.text(title).last);
final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
// The title widget should be to the left of the search icon.
expect(titleOffset.dx, lessThan(searchOffset.dx));
});
testWidgets(
'SliverAppBar.large collapsed title does not overlap with leading/actions widgets',
(WidgetTester tester) async {
const String title = 'Large SliverAppBar Very Long Title';
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 200),
sliver: SliverAppBar.large(
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
),
title: const Text(title, maxLines: 1),
centerTitle: true,
actions: const <Widget>[
Icon(Icons.search),
Icon(Icons.sort),
Icon(Icons.more_vert),
],
),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
));
// Scroll to collapse the SliverAppBar.
final ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
await tester.pumpAndSettle();
final Offset leadingOffset = tester.getTopRight(find.byIcon(Icons.menu));
Offset titleOffset = tester.getTopLeft(find.text(title).last);
// The title widget should be to the right of the leading widget.
expect(titleOffset.dx, greaterThan(leadingOffset.dx));
titleOffset = tester.getTopRight(find.text(title).last);
final Offset searchOffset = tester.getTopLeft(find.byIcon(Icons.search));
// The title widget should be to the left of the search icon.
expect(titleOffset.dx, lessThan(searchOffset.dx));
});
testWidgets('SliverAppBar.medium respects title spacing', (WidgetTester tester) async {
const String title = 'Medium SliverAppBar Very Long Title';
const double titleSpacing = 16.0;
Widget buildWidget({double? titleSpacing, bool? centerTitle}) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 200),
sliver: SliverAppBar.medium(
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title, maxLines: 1),
centerTitle: centerTitle,
titleSpacing: titleSpacing,
actions: <Widget>[
IconButton(onPressed: () {}, icon: const Icon(Icons.sort)),
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
],
),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
);
}
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(120);
await tester.pumpAndSettle();
// By default, title widget should be to the right of the
// leading widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(collapsedTitle);
Offset iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);
await tester.pumpWidget(buildWidget(centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(120);
await tester.pumpAndSettle();
// By default, title widget should be to the left of the first
// trailing widget and title spacing should be respected.
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);
// Test custom title spacing, set to 0.0.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(120);
await tester.pumpAndSettle();
// The title widget should be to the right of the leading
// widget with no spacing.
titleOffset = tester.getTopLeft(collapsedTitle);
iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
expect(titleOffset.dx, iconButtonOffset.dx);
// Set centerTitle to true so the end of the title can reach
// the action widgets.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(120);
await tester.pumpAndSettle();
// The title widget should be to the left of the first
// leading widget with no spacing.
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx);
});
testWidgets('SliverAppBar.large respects title spacing', (WidgetTester tester) async {
const String title = 'Large SliverAppBar Very Long Title';
const double titleSpacing = 16.0;
Widget buildWidget({double? titleSpacing, bool? centerTitle}) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 200),
sliver: SliverAppBar.large(
leading: IconButton(
onPressed: () {},
icon: const Icon(Icons.menu),
),
title: const Text(title, maxLines: 1),
centerTitle: centerTitle,
titleSpacing: titleSpacing,
actions: <Widget>[
IconButton(onPressed: () {}, icon: const Icon(Icons.sort)),
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
],
),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
);
}
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(160);
await tester.pumpAndSettle();
// By default, title widget should be to the right of the leading
// widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(collapsedTitle);
Offset iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
expect(titleOffset.dx, iconButtonOffset.dx + titleSpacing);
await tester.pumpWidget(buildWidget(centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(160);
await tester.pumpAndSettle();
// By default, title widget should be to the left of the
// leading widget and title spacing should be respected.
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx - titleSpacing);
// Test custom title spacing, set to 0.0.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0));
controller = primaryScrollController(tester);
controller.jumpTo(160);
await tester.pumpAndSettle();
// The title widget should be to the right of the leading
// widget with no spacing.
titleOffset = tester.getTopLeft(collapsedTitle);
iconButtonOffset = tester.getTopRight(find.ancestor(of: find.widgetWithIcon(IconButton, Icons.menu), matching: find.byType(ConstrainedBox)));
expect(titleOffset.dx, iconButtonOffset.dx);
// Set centerTitle to true so the end of the title can reach
// the action widgets.
await tester.pumpWidget(buildWidget(titleSpacing: 0.0, centerTitle: true));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(160);
await tester.pumpAndSettle();
// The title widget should be to the left of the first
// leading widget with no spacing.
titleOffset = tester.getTopRight(collapsedTitle);
iconButtonOffset = tester.getTopLeft(find.widgetWithIcon(IconButton, Icons.sort));
expect(titleOffset.dx, iconButtonOffset.dx);
});
testWidgets(
'SliverAppBar.medium without the leading widget updates collapsed title padding',
(WidgetTester tester) async {
const String title = 'Medium SliverAppBar Title';
const double leadingPadding = 56.0;
const double titleSpacing = 16.0;
Widget buildWidget({ bool showLeading = true }) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.medium(
automaticallyImplyLeading: false,
leading: showLeading
? IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
)
: null,
title: const Text(title),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
);
}
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll to collapse the SliverAppBar.
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
await tester.pumpAndSettle();
// If the leading widget is present, the title widget should be to the
// right of the leading widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, leadingPadding + titleSpacing);
// Hide the leading widget.
await tester.pumpWidget(buildWidget(showLeading: false));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(45);
await tester.pumpAndSettle();
// If the leading widget is not present, the title widget will
// only have the default title spacing.
titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, titleSpacing);
});
testWidgets(
'SliverAppBar.large without the leading widget updates collapsed title padding',
(WidgetTester tester) async {
const String title = 'Large SliverAppBar Title';
const double leadingPadding = 56.0;
const double titleSpacing = 16.0;
Widget buildWidget({ bool showLeading = true }) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar.large(
automaticallyImplyLeading: false,
leading: showLeading
? IconButton(
icon: const Icon(Icons.menu),
onPressed: () {},
)
: null,
title: const Text(title),
),
SliverToBoxAdapter(
child: Container(
height: 1200,
color: Colors.orange[400],
),
),
],
),
),
);
}
await tester.pumpWidget(buildWidget());
final Finder collapsedTitle = find.text(title).last;
// Scroll CustomScrollView to collapse SliverAppBar.
ScrollController controller = primaryScrollController(tester);
controller.jumpTo(45);
await tester.pumpAndSettle();
// If the leading widget is present, the title widget should be to the
// right of the leading widget and title spacing should be respected.
Offset titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, leadingPadding + titleSpacing);
// Hide the leading widget.
await tester.pumpWidget(buildWidget(showLeading: false));
// Scroll to collapse the SliverAppBar.
controller = primaryScrollController(tester);
controller.jumpTo(45);
await tester.pumpAndSettle();
// If the leading widget is not present, the title widget will
// only have the default title spacing.
titleOffset = tester.getTopLeft(collapsedTitle);
expect(titleOffset.dx, titleSpacing);
});
group('MaterialStateColor scrolledUnder', () {
const double collapsedHeight = kToolbarHeight;
const double expandedHeight = 200.0;
const Color scrolledColor = Color(0xff00ff00);
const Color defaultColor = Color(0xff0000ff);
Widget buildSliverApp({
required double contentHeight,
bool reverse = false,
bool includeFlexibleSpace = false,
}) {
return MaterialApp(
home: Scaffold(
body: CustomScrollView(
reverse: reverse,
slivers: <Widget>[
SliverAppBar(
elevation: 0,
backgroundColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
return states.contains(MaterialState.scrolledUnder)
? scrolledColor
: defaultColor;
}),
expandedHeight: expandedHeight,
pinned: true,
flexibleSpace: includeFlexibleSpace
? const FlexibleSpaceBar(title: Text('SliverAppBar'))
: null,
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
Container(height: contentHeight, color: Colors.teal),
],
),
),
],
),
),
);
}
testWidgets('backgroundColor', (WidgetTester tester) async {
await tester.pumpWidget(
buildSliverApp(contentHeight: 1200.0)
);
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
testWidgets('backgroundColor with FlexibleSpace', (WidgetTester tester) async {
await tester.pumpWidget(
buildSliverApp(contentHeight: 1200.0, includeFlexibleSpace: true)
);
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
testWidgets('backgroundColor - reverse', (WidgetTester tester) async {
await tester.pumpWidget(
buildSliverApp(contentHeight: 1200.0, reverse: true)
);
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, -expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
testWidgets('backgroundColor with FlexibleSpace - reverse', (WidgetTester tester) async {
await tester.pumpWidget(
buildSliverApp(
contentHeight: 1200.0,
reverse: true,
includeFlexibleSpace: true,
)
);
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), scrolledColor);
expect(tester.getSize(findAppBarMaterial()).height, collapsedHeight);
gesture = await tester.startGesture(const Offset(50.0, 300.0));
await gesture.moveBy(const Offset(0.0, -expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
testWidgets('backgroundColor - not triggered in reverse for short content', (WidgetTester tester) async {
await tester.pumpWidget(
buildSliverApp(contentHeight: 200, reverse: true)
);
// In reverse, the content here is not long enough to scroll under the app
// bar.
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
testWidgets('backgroundColor with FlexibleSpace - not triggered in reverse for short content', (WidgetTester tester) async {
await tester.pumpWidget(
buildSliverApp(
contentHeight: 200,
reverse: true,
includeFlexibleSpace: true,
)
);
// In reverse, the content here is not long enough to scroll under the app
// bar.
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
final TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, expandedHeight));
await gesture.up();
await tester.pumpAndSettle();
expect(getAppBarBackgroundColor(tester), defaultColor);
expect(tester.getSize(findAppBarMaterial()).height, expandedHeight);
});
});
testWidgets('SliverAppBar default configuration', (WidgetTester tester) async {
await tester.pumpWidget(buildSliverAppBarApp());
final ScrollController controller = primaryScrollController(tester);
expect(controller.offset, 0.0);
expect(find.byType(SliverAppBar), findsOneWidget);
final double initialAppBarHeight = appBarHeight(tester);
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar partially out of view
controller.jumpTo(50.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), initialAppBarHeight);
expect(tabBarHeight(tester), initialTabBarHeight);
// Scroll the not-pinned appbar out of view
controller.jumpTo(600.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsNothing);
expect(appBarHeight(tester), initialAppBarHeight);
expect(tabBarHeight(tester), initialTabBarHeight);
// Scroll the not-pinned appbar back into view
controller.jumpTo(0.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), initialAppBarHeight);
expect(tabBarHeight(tester), initialTabBarHeight);
});
testWidgets('SliverAppBar expandedHeight, pinned', (WidgetTester tester) async {
await tester.pumpWidget(buildSliverAppBarApp(
pinned: true,
expandedHeight: 128.0,
));
final ScrollController controller = primaryScrollController(tester);
expect(controller.offset, 0.0);
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), 128.0);
const double initialAppBarHeight = 128.0;
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar, collapsing the expanded height. At this
// point both the toolbar and the tabbar are visible.
controller.jumpTo(600.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(tabBarHeight(tester), initialTabBarHeight);
expect(appBarHeight(tester), lessThan(initialAppBarHeight));
expect(appBarHeight(tester), greaterThan(initialTabBarHeight));
// Scroll the not-pinned appbar back into view
controller.jumpTo(0.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), initialAppBarHeight);
expect(tabBarHeight(tester), initialTabBarHeight);
});
testWidgets('SliverAppBar expandedHeight, pinned and floating', (WidgetTester tester) async {
await tester.pumpWidget(buildSliverAppBarApp(
floating: true,
pinned: true,
expandedHeight: 128.0,
));
final ScrollController controller = primaryScrollController(tester);
expect(controller.offset, 0.0);
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), 128.0);
const double initialAppBarHeight = 128.0;
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the floating-pinned appbar, collapsing the expanded height. At this
// point only the tabBar is visible.
controller.jumpTo(600.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(tabBarHeight(tester), initialTabBarHeight);
expect(appBarHeight(tester), lessThan(initialAppBarHeight));
expect(appBarHeight(tester), initialTabBarHeight);
// Scroll the floating-pinned appbar back into view
controller.jumpTo(0.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), initialAppBarHeight);
expect(tabBarHeight(tester), initialTabBarHeight);
});
testWidgets('SliverAppBar expandedHeight, floating with snap:true', (WidgetTester tester) async {
await tester.pumpWidget(buildSliverAppBarApp(
floating: true,
snap: true,
expandedHeight: 128.0,
));
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), 128.0);
expect(appBarBottom(tester), 128.0);
// Scroll to the middle of the list. The (floating) appbar is no longer visible.
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.jumpTo(256.00);
await tester.pumpAndSettle();
expect(find.byType(SliverAppBar), findsNothing);
expect(appBarTop(tester), lessThanOrEqualTo(-128.0));
// Drag the scrollable up and down. The app bar should not snap open, its
// height should just track the drag offset.
TestGesture gesture = await tester.startGesture(const Offset(50.0, 256.0));
await gesture.moveBy(const Offset(0.0, 128.0)); // drag the appbar all the way open
await tester.pump();
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), 128.0);
await gesture.moveBy(const Offset(0.0, -50.0));
await tester.pump();
expect(appBarBottom(tester), 78.0); // 78 == 128 - 50
// Trigger the snap open animation: drag down and release
await gesture.moveBy(const Offset(0.0, 10.0));
await gesture.up();
// Now verify that the appbar is animating open
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
double bottom = appBarBottom(tester);
expect(bottom, greaterThan(88.0)); // 88 = 78 + 10
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(appBarBottom(tester), greaterThan(bottom));
// The animation finishes when the appbar is full height.
await tester.pumpAndSettle();
expect(appBarHeight(tester), 128.0);
// Now that the app bar is open, perform the same drag scenario
// in reverse: drag the appbar up and down and then trigger the
// snap closed animation.
gesture = await tester.startGesture(const Offset(50.0, 256.0));
await gesture.moveBy(const Offset(0.0, -128.0)); // drag the appbar closed
await tester.pump();
expect(appBarBottom(tester), 0.0);
await gesture.moveBy(const Offset(0.0, 100.0));
await tester.pump();
expect(appBarBottom(tester), 100.0);
// Trigger the snap close animation: drag upwards and release
await gesture.moveBy(const Offset(0.0, -10.0));
await gesture.up();
// Now verify that the appbar is animating closed
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
bottom = appBarBottom(tester);
expect(bottom, lessThan(90.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(appBarBottom(tester), lessThan(bottom));
// The animation finishes when the appbar is off screen.
await tester.pumpAndSettle();
expect(appBarTop(tester), lessThanOrEqualTo(0.0));
expect(appBarBottom(tester), lessThanOrEqualTo(0.0));
});
testWidgets('SliverAppBar expandedHeight, floating and pinned with snap:true', (WidgetTester tester) async {
await tester.pumpWidget(buildSliverAppBarApp(
floating: true,
pinned: true,
snap: true,
expandedHeight: 128.0,
));
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), 128.0);
expect(appBarBottom(tester), 128.0);
// Scroll to the middle of the list. The only the tab bar is visible
// because this is a pinned appbar.
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
position.jumpTo(256.0);
await tester.pumpAndSettle();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), kTextTabBarHeight);
// Drag the scrollable up and down. The app bar should not snap open, the
// bottom of the appbar should just track the drag offset.
TestGesture gesture = await tester.startGesture(const Offset(50.0, 200.0));
await gesture.moveBy(const Offset(0.0, 100.0));
await tester.pump();
expect(appBarHeight(tester), 100.0);
await gesture.moveBy(const Offset(0.0, -25.0));
await tester.pump();
expect(appBarHeight(tester), 75.0);
// Trigger the snap animation: drag down and release
await gesture.moveBy(const Offset(0.0, 10.0));
await gesture.up();
// Now verify that the appbar is animating open
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
final double height = appBarHeight(tester);
expect(height, greaterThan(85.0));
expect(height, lessThan(128.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(appBarHeight(tester), greaterThan(height));
expect(appBarHeight(tester), lessThan(128.0));
// The animation finishes when the appbar is fully expanded
await tester.pumpAndSettle();
expect(appBarTop(tester), 0.0);
expect(appBarHeight(tester), 128.0);
expect(appBarBottom(tester), 128.0);
// Now that the appbar is fully expanded, Perform the same drag
// scenario in reverse: drag the appbar up and down and then trigger
// the snap closed animation.
gesture = await tester.startGesture(const Offset(50.0, 256.0));
await gesture.moveBy(const Offset(0.0, -128.0));
await tester.pump();
expect(appBarBottom(tester), kTextTabBarHeight);
await gesture.moveBy(const Offset(0.0, 100.0));
await tester.pump();
expect(appBarBottom(tester), 100.0);
// Trigger the snap close animation: drag upwards and release
await gesture.moveBy(const Offset(0.0, -10.0));
await gesture.up();
// Now verify that the appbar is animating closed
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
final double bottom = appBarBottom(tester);
expect(bottom, lessThan(90.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 50));
expect(appBarBottom(tester), lessThan(bottom));
// The animation finishes when the appbar shrinks back to its pinned height
await tester.pumpAndSettle();
expect(appBarTop(tester), lessThanOrEqualTo(0.0));
expect(appBarBottom(tester), kTextTabBarHeight);
});
testWidgets('SliverAppBar expandedHeight, collapsedHeight', (WidgetTester tester) async {
const double expandedAppBarHeight = 400.0;
const double collapsedAppBarHeight = 200.0;
await tester.pumpWidget(buildSliverAppBarApp(
collapsedHeight: collapsedAppBarHeight,
expandedHeight: expandedAppBarHeight,
));
final ScrollController controller = primaryScrollController(tester);
expect(controller.offset, 0.0);
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight);
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar partially out of view.
controller.jumpTo(50.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight - 50.0);
expect(tabBarHeight(tester), initialTabBarHeight);
// Scroll the not-pinned appbar out of view, to its collapsed height.
controller.jumpTo(600.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsNothing);
expect(appBarHeight(tester), collapsedAppBarHeight + initialTabBarHeight);
expect(tabBarHeight(tester), initialTabBarHeight);
// Scroll the not-pinned appbar back into view.
controller.jumpTo(0.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsOneWidget);
expect(appBarHeight(tester), expandedAppBarHeight);
expect(tabBarHeight(tester), initialTabBarHeight);
});
testWidgets('Material3 - SliverAppBar.medium defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 112;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
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),
).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.
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.dx, 16.0);
if (!kIsWeb || isSkiaWeb) {
expect(titleOffset.dy, 96.0);
}
verifyTextNotClipped(expandedTitle, tester);
// Test the expanded title default color.
expect(
tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
theme.colorScheme.onSurface,
);
// Scroll the expanded app bar partially out of view.
controller.jumpTo(45);
await tester.pump();
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('Material3 - SliverAppBar.large defaults', (WidgetTester tester) async {
final ThemeData theme = ThemeData(useMaterial3: true);
const double collapsedAppBarHeight = 64;
const double expandedAppBarHeight = 152;
await tester.pumpWidget(MaterialApp(
theme: theme,
home: Scaffold(
body: CustomScrollView(
primary: true,
slivers: <Widget>[
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),
).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.
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.dx, 16.0);
final RenderSliver renderSliverAppBar = tester.renderObject(find.byType(SliverAppBar));
// The expanded title and the bottom padding fits in the flexible space.
expect(
titleOffset.dy,
renderSliverAppBar.geometry!.scrollExtent - 28.0,
reason: 'bottom padding of a large expanded title should be 28.',
);
verifyTextNotClipped(expandedTitle, tester);
// Test the expanded title default color.
expect(
tester.renderObject<RenderParagraph>(expandedTitle).text.style!.color,
theme.colorScheme.onSurface,
);
// Scroll the expanded app bar partially out of view.
controller.jumpTo(45);
await tester.pump();
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);
});
group('SliverAppBar elevation', () {
Widget buildSliverAppBar(bool forceElevated, {double? elevation, double? themeElevation}) {
return MaterialApp(
theme: ThemeData(
appBarTheme: AppBarTheme(
elevation: themeElevation,
scrolledUnderElevation: themeElevation,
),
),
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: const Text('Title'),
forceElevated: forceElevated,
elevation: elevation,
scrolledUnderElevation: elevation,
),
],
),
);
}
testWidgets('Respects forceElevated parameter', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/59158.
AppBar getAppBar() => tester.widget<AppBar>(find.byType(AppBar));
Material getMaterial() => tester.widget<Material>(find.byType(Material));
final bool useMaterial3 = ThemeData().useMaterial3;
// When forceElevated is off, SliverAppBar should not be elevated.
await tester.pumpWidget(buildSliverAppBar(false));
expect(getMaterial().elevation, 0.0);
// Default elevation should be used by the material, but
// the AppBar's elevation should not be specified by SliverAppBar.
// When useMaterial3 is true, and forceElevated is true, the default elevation
// should be the value of `scrolledUnderElevation` which is 3.0
await tester.pumpWidget(buildSliverAppBar(true));
expect(getMaterial().elevation, useMaterial3 ? 3.0 : 4.0);
expect(getAppBar().elevation, null);
// SliverAppBar should use the specified elevation.
await tester.pumpWidget(buildSliverAppBar(true, elevation: 8.0));
expect(getMaterial().elevation, 8.0);
});
testWidgets('Uses elevation of AppBarTheme by default', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/73525.
Material getMaterial() => tester.widget<Material>(find.byType(Material));
await tester.pumpWidget(buildSliverAppBar(false, themeElevation: 12.0));
expect(getMaterial().elevation, 0.0);
await tester.pumpWidget(buildSliverAppBar(true, themeElevation: 12.0));
expect(getMaterial().elevation, 12.0);
await tester.pumpWidget(buildSliverAppBar(true, elevation: 8.0, themeElevation: 12.0));
expect(getMaterial().elevation, 8.0);
});
});
group('SliverAppBar.forceMaterialTransparency', () {
Material getSliverAppBarMaterial(WidgetTester tester) {
return tester.widget<Material>(find
.descendant(of: find.byType(SliverAppBar), matching: find.byType(Material))
.first);
}
// Generates a MaterialApp with a SliverAppBar in a CustomScrollView.
// The first cell of the scroll view contains a button at its top, and is
// initially scrolled so that it is beneath the SliverAppBar.
(ScrollController, Widget) buildWidget({
required bool forceMaterialTransparency,
required VoidCallback onPressed
}) {
const double appBarHeight = 120;
final ScrollController controller = ScrollController(initialScrollOffset: appBarHeight);
return (
controller,
MaterialApp(
home: Scaffold(
body: CustomScrollView(
controller: controller,
slivers: <Widget>[
SliverAppBar(
collapsedHeight: appBarHeight,
expandedHeight: appBarHeight,
pinned: true,
elevation: 0,
backgroundColor: Colors.transparent,
forceMaterialTransparency: forceMaterialTransparency,
title: const Text('AppBar'),
),
SliverList(
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return SizedBox(
height: appBarHeight,
child: index == 0
? Align(
alignment: Alignment.topCenter,
child: TextButton(onPressed: onPressed, child: const Text('press')))
: const SizedBox(),
);
},
childCount: 20,
),
),
]),
),
),
);
}
testWidgets(
'forceMaterialTransparency == true allows gestures beneath the app bar', (WidgetTester tester) async {
bool buttonWasPressed = false;
final (ScrollController controller, Widget widget) = buildWidget(
forceMaterialTransparency:true,
onPressed:() { buttonWasPressed = true; },
);
await tester.pumpWidget(widget);
final Material material = getSliverAppBarMaterial(tester);
expect(material.type, MaterialType.transparency);
final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder);
await tester.pump();
expect(buttonWasPressed, isTrue);
controller.dispose();
});
testWidgets(
'forceMaterialTransparency == false does not allow gestures beneath the app bar', (WidgetTester tester) async {
// Set this, and tester.tap(warnIfMissed:false), to suppress
// errors/warning that the button is not hittable (which is expected).
WidgetController.hitTestWarningShouldBeFatal = false;
bool buttonWasPressed = false;
final (ScrollController controller, Widget widget) = buildWidget(
forceMaterialTransparency:false,
onPressed:() { buttonWasPressed = true; },
);
await tester.pumpWidget(widget);
final Material material = getSliverAppBarMaterial(tester);
expect(material.type, MaterialType.canvas);
final Finder buttonFinder = find.byType(TextButton);
await tester.tap(buttonFinder, warnIfMissed:false);
await tester.pump();
expect(buttonWasPressed, isFalse);
controller.dispose();
});
});
testWidgets('SliverAppBar positioning of leading and trailing widgets with top padding', (WidgetTester tester) async {
const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0));
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
final Key trailingKey = UniqueKey();
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: topPadding100,
child: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar(
leading: Placeholder(key: leadingKey),
title: Placeholder(key: titleKey, fallbackHeight: kToolbarHeight),
actions: <Widget>[ Placeholder(key: trailingKey) ],
),
],
),
),
),
),
);
expect(tester.getTopLeft(find.byType(AppBar)), Offset.zero);
expect(tester.getTopLeft(find.byKey(leadingKey)), const Offset(800.0 - 56.0, 100.0));
expect(tester.getTopLeft(find.byKey(titleKey)), const Offset(416.0, 100.0));
expect(tester.getTopLeft(find.byKey(trailingKey)), const Offset(0.0, 100.0));
});
testWidgets('SliverAppBar positioning of leading and trailing widgets with bottom padding', (WidgetTester tester) async {
const MediaQueryData topPadding100 = MediaQueryData(padding: EdgeInsets.only(top: 100.0, bottom: 50.0));
final Key leadingKey = UniqueKey();
final Key titleKey = UniqueKey();
final Key trailingKey = UniqueKey();
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'US'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.rtl,
child: MediaQuery(
data: topPadding100,
child: CustomScrollView(
primary: true,
slivers: <Widget>[
SliverAppBar(
leading: Placeholder(key: leadingKey),
title: Placeholder(key: titleKey),
actions: <Widget>[ Placeholder(key: trailingKey) ],
),
],
),
),
),
),
);
expect(tester.getRect(find.byType(AppBar)), const Rect.fromLTRB(0.0, 0.0, 800.00, 100.0 + 56.0));
expect(tester.getRect(find.byKey(leadingKey)), const Rect.fromLTRB(800.0 - 56.0, 100.0, 800.0, 100.0 + 56.0));
expect(tester.getRect(find.byKey(trailingKey)), const Rect.fromLTRB(0.0, 100.0, 400.0, 100.0 + 56.0));
});
testWidgets('SliverAppBar provides correct semantics in LTR', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Center(
child: AppBar(
leading: const Text('Leading'),
title: const Text('Title'),
actions: const <Widget>[
Text('Action 1'),
Text('Action 2'),
Text('Action 3'),
],
bottom: const PreferredSize(
preferredSize: Size(0.0, kToolbarHeight),
child: Text('Bottom'),
),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics> [
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Leading',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.namesRoute,
SemanticsFlag.isHeader,
],
label: 'Title',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Action 1',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Action 2',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Action 3',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Bottom',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
));
semantics.dispose();
});
testWidgets('SliverAppBar provides correct semantics in RTL', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
home: Semantics(
textDirection: TextDirection.rtl,
child: Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: AppBar(
leading: const Text('Leading'),
title: const Text('Title'),
actions: const <Widget>[
Text('Action 1'),
Text('Action 2'),
Text('Action 3'),
],
bottom: const PreferredSize(
preferredSize: Size(0.0, kToolbarHeight),
child: Text('Bottom'),
),
),
),
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.rtl,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Leading',
textDirection: TextDirection.rtl,
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.namesRoute,
SemanticsFlag.isHeader,
],
label: 'Title',
textDirection: TextDirection.rtl,
),
TestSemantics(
label: 'Action 1',
textDirection: TextDirection.rtl,
),
TestSemantics(
label: 'Action 2',
textDirection: TextDirection.rtl,
),
TestSemantics(
label: 'Action 3',
textDirection: TextDirection.rtl,
),
TestSemantics(
label: 'Bottom',
textDirection: TextDirection.rtl,
),
],
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
));
semantics.dispose();
});
testWidgets('SliverAppBar excludes header semantics correctly', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: Text('Leading'),
flexibleSpace: ExcludeSemantics(child: Text('Title')),
actions: <Widget>[Text('Action 1')],
excludeHeaderSemantics: true,
),
],
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Leading',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Action 1',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
));
semantics.dispose();
});
testWidgets('SliverAppBar with flexible space has correct semantics order', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/64922.
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: Text('Leading'),
flexibleSpace: Text('Flexible space'),
actions: <Widget>[Text('Action 1')],
),
],
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Leading',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'Action 1',
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isHeader],
label: 'Flexible space',
textDirection: TextDirection.ltr,
),
],
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
),
],
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
ignoreId: true,
));
semantics.dispose();
});
testWidgets('Changing SliverAppBar snap from true to false', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/17598
const double appBarHeight = 256.0;
bool snap = true;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: appBarHeight,
floating: true,
snap: snap,
actions: <Widget>[
TextButton(
child: const Text('snap=false'),
onPressed: () {
setState(() {
snap = false;
});
},
),
],
flexibleSpace: FlexibleSpaceBar(
background: Container(
height: appBarHeight,
color: Colors.orange,
),
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
Container(height: 1200.0, color: Colors.teal),
],
),
),
],
),
);
},
),
),
);
TestGesture gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -100.0));
await gesture.up();
await tester.tap(find.text('snap=false'));
await tester.pumpAndSettle();
gesture = await tester.startGesture(const Offset(50.0, 400.0));
await gesture.moveBy(const Offset(0.0, -100.0));
await gesture.up();
await tester.pump();
});
testWidgets('SliverAppBar shape default', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: Text('L'),
title: Text('No Scaffold'),
actions: <Widget>[Text('A1'), Text('A2')],
),
],
),
),
);
final Finder sliverAppBarFinder = find.byType(SliverAppBar);
SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget<SliverAppBar>(finder);
expect(getSliverAppBarWidget(sliverAppBarFinder).shape, null);
final Finder materialFinder = find.byType(Material);
Material getMaterialWidget(Finder finder) => tester.widget<Material>(finder);
expect(getMaterialWidget(materialFinder).shape, null);
});
testWidgets('SliverAppBar with shape', (WidgetTester tester) async {
const RoundedRectangleBorder roundedRectangleBorder = RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
);
await tester.pumpWidget(
const MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: Text('L'),
title: Text('No Scaffold'),
actions: <Widget>[Text('A1'), Text('A2')],
shape: roundedRectangleBorder,
),
],
),
),
);
final Finder sliverAppBarFinder = find.byType(SliverAppBar);
SliverAppBar getSliverAppBarWidget(Finder finder) => tester.widget<SliverAppBar>(finder);
expect(getSliverAppBarWidget(sliverAppBarFinder).shape, roundedRectangleBorder);
final Finder materialFinder = find.byType(Material);
Material getMaterialWidget(Finder finder) => tester.widget<Material>(finder);
expect(getMaterialWidget(materialFinder).shape, roundedRectangleBorder);
});
testWidgets('SliverAppBar configures the delegate properly', (WidgetTester tester) async {
Future<void> buildAndVerifyDelegate({ required bool pinned, required bool floating, required bool snap }) async {
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
title: const Text('Jumbo'),
pinned: pinned,
floating: floating,
snap: snap,
),
],
),
),
);
final SliverPersistentHeaderDelegate delegate = tester
.widget<SliverPersistentHeader>(find.byType(SliverPersistentHeader))
.delegate;
// Ensure we have a non-null vsync when it's needed.
if (!floating || (delegate.snapConfiguration == null && delegate.showOnScreenConfiguration == null)) {
expect(delegate.vsync, isNotNull);
}
expect(delegate.showOnScreenConfiguration != null, snap && floating);
}
await buildAndVerifyDelegate(pinned: false, floating: true, snap: false);
await buildAndVerifyDelegate(pinned: false, floating: true, snap: true);
await buildAndVerifyDelegate(pinned: true, floating: true, snap: false);
await buildAndVerifyDelegate(pinned: true, floating: true, snap: true);
});
testWidgets('SliverAppBar default collapsedHeight with respect to toolbarHeight', (WidgetTester tester) async {
const double toolbarHeight = 100.0;
await tester.pumpWidget(buildSliverAppBarApp(
toolbarHeight: toolbarHeight,
));
final ScrollController controller = primaryScrollController(tester);
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar out of view, to its collapsed height.
controller.jumpTo(300.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsNothing);
// By default, the collapsedHeight is toolbarHeight + bottom.preferredSize.height,
// in this case initialTabBarHeight.
expect(appBarHeight(tester), toolbarHeight + initialTabBarHeight);
});
testWidgets('SliverAppBar collapsedHeight with toolbarHeight', (WidgetTester tester) async {
const double toolbarHeight = 100.0;
const double collapsedHeight = 150.0;
await tester.pumpWidget(buildSliverAppBarApp(
toolbarHeight: toolbarHeight,
collapsedHeight: collapsedHeight,
));
final ScrollController controller = primaryScrollController(tester);
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar out of view, to its collapsed height.
controller.jumpTo(300.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsNothing);
expect(appBarHeight(tester), collapsedHeight + initialTabBarHeight);
});
testWidgets('SliverAppBar collapsedHeight', (WidgetTester tester) async {
const double collapsedHeight = 56.0;
await tester.pumpWidget(buildSliverAppBarApp(
collapsedHeight: collapsedHeight,
));
final ScrollController controller = primaryScrollController(tester);
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar out of view, to its collapsed height.
controller.jumpTo(300.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsNothing);
expect(appBarHeight(tester), collapsedHeight + initialTabBarHeight);
});
testWidgets('SliverAppBar respects leadingWidth', (WidgetTester tester) async {
const Key key = Key('leading');
await tester.pumpWidget(const MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
leading: Placeholder(key: key),
leadingWidth: 100,
title: Text('Title'),
),
],
),
));
// By default toolbarHeight is 56.0.
expect(tester.getRect(find.byKey(key)), const Rect.fromLTRB(0, 0, 100, 56));
});
testWidgets('SliverAppBar.titleSpacing defaults to NavigationToolbar.kMiddleSpacing', (WidgetTester tester) async {
await tester.pumpWidget(buildSliverAppBarApp());
final NavigationToolbar navToolBar = tester.widget(find.byType(NavigationToolbar));
expect(navToolBar.middleSpacing, NavigationToolbar.kMiddleSpacing);
});
group('Material 2', () {
// These tests are only relevant for Material 2. Once Material 2
// support is deprecated and the APIs are removed, these tests
// can be deleted.
testWidgets('Material2 - 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('Material2 - 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: theme,
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);
});
});
}