blob: a669aa2ee4fd3d0fdfe5ed7eab8d37c4fb97f5f2 [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 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
void _verifySliverGeometry({
required GlobalKey key,
required bool visible,
required double paintExtent,
}) {
final target = key.currentContext!.findRenderObject()! as RenderSliver;
final SliverGeometry geometry = target.geometry!;
expect(geometry.visible, visible);
expect(geometry.paintExtent, paintExtent);
}
void main() {
Future<void> slowDrag(WidgetTester tester, Key widget, Offset offset) async {
final Offset target = tester.getCenter(find.byKey(widget));
final TestGesture gesture = await tester.startGesture(target);
await gesture.moveBy(offset);
await tester.pump(const Duration(milliseconds: 10));
await gesture.up();
}
testWidgets('SliverAppBar - floating and pinned - correct elevation', (
WidgetTester tester,
) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
bottom: PreferredSize(preferredSize: Size.fromHeight(28), child: Text('Bottom')),
backgroundColor: Colors.green,
floating: true,
primary: false,
automaticallyImplyLeading: false,
),
SliverToBoxAdapter(child: Container(color: Colors.yellow, height: 50.0)),
SliverToBoxAdapter(child: Container(color: Colors.red, height: 50.0)),
],
),
),
),
),
);
final RenderPhysicalModel renderObject = tester.renderObject<RenderPhysicalModel>(
find.byType(PhysicalModel),
);
expect(renderObject, isNotNull);
expect(renderObject.elevation, 0.0);
});
testWidgets('SliverAppBar - floating and pinned - correct semantics', (
WidgetTester tester,
) async {
await tester.pumpWidget(
Localizations(
locale: const Locale('en', 'us'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultWidgetsLocalizations.delegate,
DefaultMaterialLocalizations.delegate,
],
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
title: Text('Hello'),
pinned: true,
floating: true,
expandedHeight: 200.0,
),
SliverFixedExtentList(
itemExtent: 100.0,
delegate: SliverChildBuilderDelegate((BuildContext _, int index) {
return Container(
height: 100.0,
color: index.isEven ? Colors.red : Colors.yellow,
child: Text('Tile $index'),
);
}),
),
],
),
),
),
),
);
final semantics = SemanticsTester(tester);
var expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Hello',
flags: <SemanticsFlag>[SemanticsFlag.isHeader, SemanticsFlag.namesRoute],
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollToOffset,
],
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
scrollIndex: 0,
children: <TestSemantics>[
TestSemantics(label: 'Tile 0', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 1', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 2', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 3', textDirection: TextDirection.ltr),
TestSemantics(
label: 'Tile 4',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
TestSemantics(
label: 'Tile 5',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
TestSemantics(
label: 'Tile 6',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
],
),
],
),
],
),
],
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
);
await tester.fling(find.text('Tile 2'), const Offset(0, -600), 2000);
await tester.pumpAndSettle();
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
label: 'Hello',
flags: <SemanticsFlag>[SemanticsFlag.isHeader, SemanticsFlag.namesRoute],
textDirection: TextDirection.ltr,
),
],
),
TestSemantics(
actions: <SemanticsAction>[
SemanticsAction.scrollUp,
SemanticsAction.scrollDown,
SemanticsAction.scrollToOffset,
],
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
scrollIndex: 11,
children: <TestSemantics>[
TestSemantics(
label: 'Tile 7',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
TestSemantics(
label: 'Tile 8',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
TestSemantics(
label: 'Tile 9',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
TestSemantics(
label: 'Tile 10',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
TestSemantics(label: 'Tile 11', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 12', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 13', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 14', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 15', textDirection: TextDirection.ltr),
TestSemantics(label: 'Tile 16', textDirection: TextDirection.ltr),
TestSemantics(
label: 'Tile 17',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
TestSemantics(
label: 'Tile 18',
textDirection: TextDirection.ltr,
flags: <SemanticsFlag>[SemanticsFlag.isHidden],
),
],
),
],
),
],
),
],
);
expect(
semantics,
hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true),
);
semantics.dispose();
});
testWidgets('SliverAppBar - floating and pinned - second app bar stacks below', (
WidgetTester tester,
) async {
final controller = ScrollController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(useMaterial3: false),
home: CustomScrollView(
controller: controller,
slivers: <Widget>[
const SliverAppBar(
floating: true,
pinned: true,
expandedHeight: 200.0,
title: Text('A'),
),
const SliverAppBar(primary: false, pinned: true, title: Text('B')),
SliverList.list(
children: const <Widget>[
Text('C'),
Text('D'),
SizedBox(height: 500.0),
Text('E'),
SizedBox(height: 500.0),
],
),
],
),
),
);
const textPositionInAppBar = Offset(16.0, 18.0);
expect(tester.getTopLeft(find.text('A')), textPositionInAppBar);
// top app bar is 200.0 high at this point
expect(tester.getTopLeft(find.text('B')), const Offset(0.0, 200.0) + textPositionInAppBar);
// second app bar is 56.0 high
expect(
tester.getTopLeft(find.text('C')),
const Offset(0.0, 200.0 + 56.0),
); // height of both appbars
final Size cSize = tester.getSize(find.text('C'));
controller.jumpTo(200.0 - 56.0);
await tester.pump();
expect(tester.getTopLeft(find.text('A')), textPositionInAppBar);
// top app bar is now only 56.0 high, same as second
expect(tester.getTopLeft(find.text('B')), const Offset(0.0, 56.0) + textPositionInAppBar);
expect(
tester.getTopLeft(find.text('C')),
const Offset(0.0, 56.0 * 2.0),
); // height of both collapsed appbars
expect(find.text('E'), findsNothing);
controller.jumpTo(600.0);
await tester.pump();
expect(tester.getTopLeft(find.text('A')), textPositionInAppBar); // app bar is pinned at top
expect(
tester.getTopLeft(find.text('B')),
const Offset(0.0, 56.0) + textPositionInAppBar,
); // second one too
expect(find.text('C'), findsNothing); // contents are scrolled off though
expect(find.text('D'), findsNothing);
// we have scrolled 600.0 pixels
// initial position of E was 200 + 56 + cSize.height + cSize.height + 500
// we've scrolled that up by 600.0, meaning it's at that minus 600 now:
expect(
tester.getTopLeft(find.text('E')),
Offset(0.0, 200.0 + 56.0 + cSize.height * 2.0 + 500.0 - 600.0),
);
});
testWidgets(
'SliverAppBar does not crash when there is less than minExtent remainingPaintExtent',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/21887.
final controller = ScrollController();
addTearDown(controller.dispose);
const availableHeight = 50.0;
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Container(
height: availableHeight,
color: Colors.green,
child: CustomScrollView(
controller: controller,
slivers: <Widget>[
const SliverAppBar(pinned: true, floating: true, expandedHeight: 120.0),
SliverList.builder(
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return SizedBox(height: 100.0, child: Text('Tile $index'));
},
),
],
),
),
),
),
);
final RenderSliverFloatingPinnedPersistentHeader render = tester.renderObject(
find.byType(SliverAppBar),
);
expect(render.minExtent, greaterThan(availableHeight)); // Precondition
expect(render.geometry!.scrollExtent, 120.0);
expect(render.geometry!.paintExtent, availableHeight);
expect(render.geometry!.layoutExtent, availableHeight);
controller.jumpTo(200.0);
await tester.pumpAndSettle();
expect(render.geometry!.scrollExtent, 120.0);
expect(render.geometry!.paintExtent, availableHeight);
expect(render.geometry!.layoutExtent, 0.0);
},
);
testWidgets('Pinned and floating SliverAppBar sticks to top the content is scroll down', (
WidgetTester tester,
) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Container(
height: 300,
color: Colors.green,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(pinned: true, floating: true, expandedHeight: 100.0),
SliverToBoxAdapter(
child: Container(key: anchor, color: Colors.red, height: 100),
),
SliverToBoxAdapter(child: Container(height: 600, color: Colors.green)),
],
),
),
),
),
);
final RenderSliverFloatingPinnedPersistentHeader render = tester.renderObject(
find.byType(SliverAppBar),
);
const double scrollDistance = 40;
final TestGesture gesture = await tester.press(find.byKey(anchor));
await gesture.moveBy(const Offset(0, scrollDistance));
await tester.pump();
expect(render.geometry!.paintOrigin, -scrollDistance);
});
testWidgets('Floating SliverAppBar sticks to top the content is scroll down', (
WidgetTester tester,
) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Container(
height: 300,
color: Colors.green,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(floating: true, expandedHeight: 100.0),
SliverToBoxAdapter(
child: Container(key: anchor, color: Colors.red, height: 100),
),
SliverToBoxAdapter(child: Container(height: 600, color: Colors.green)),
],
),
),
),
),
);
final RenderSliverFloatingPersistentHeader render = tester.renderObject(
find.byType(SliverAppBar),
);
const double scrollDistance = 40;
final TestGesture gesture = await tester.press(find.byKey(anchor));
await gesture.moveBy(const Offset(0, scrollDistance));
await tester.pump();
expect(render.geometry!.paintOrigin, -scrollDistance);
});
testWidgets('Pinned SliverAppBar sticks to top the content is scroll down', (
WidgetTester tester,
) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Container(
height: 300,
color: Colors.green,
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(pinned: true, expandedHeight: 100.0),
SliverToBoxAdapter(
child: Container(key: anchor, color: Colors.red, height: 100),
),
SliverToBoxAdapter(child: Container(height: 600, color: Colors.green)),
],
),
),
),
),
);
final RenderSliverPinnedPersistentHeader render = tester.renderObject(
find.byType(SliverAppBar),
);
const double scrollDistance = 40;
final TestGesture gesture = await tester.press(find.byKey(anchor));
await gesture.moveBy(const Offset(0, scrollDistance));
await tester.pump();
expect(render.geometry!.paintOrigin, -scrollDistance);
});
testWidgets('Pointer scrolled floating and SliverAppBar', (WidgetTester tester) async {
final GlobalKey appBarKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(key: appBarKey, floating: true, title: const Text('Test Title')),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Text('Item $index'),
childCount: 30,
),
),
],
),
),
);
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 5'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 56.0);
_verifySliverGeometry(key: appBarKey, visible: true, paintExtent: 56.0);
// Pointer scroll the app bar away, we will scroll back less to validate the
// app bar floats back in.
final Offset point1 = tester.getCenter(find.text('Item 5'));
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
testPointer.hover(point1);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
await tester.pump();
expect(find.text('Test Title'), findsNothing);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
_verifySliverGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
// Scroll back to float in appbar
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -50.0)));
await tester.pump();
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 56.0);
_verifySliverGeometry(key: appBarKey, paintExtent: 50.0, visible: true);
// Float the rest of the way in.
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -250.0)));
await tester.pump();
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 5'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 56.0);
_verifySliverGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
});
testWidgets('Pointer scrolled floating and snapping SliverAppBar', (WidgetTester tester) async {
final GlobalKey appBarKey = GlobalKey();
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
key: appBarKey,
floating: true,
snap: true,
title: const Text('Test Title'),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => Text('Item $index'),
childCount: 30,
),
),
],
),
),
);
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('Item 1'), findsOneWidget);
expect(find.text('Item 5'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 56.0);
_verifySliverGeometry(key: appBarKey, visible: true, paintExtent: 56.0);
// Pointer scroll the app bar away, we will scroll back less to validate the
// app bar floats back in and then snaps to full size.
final Offset point1 = tester.getCenter(find.text('Item 5'));
final testPointer = TestPointer(1, ui.PointerDeviceKind.mouse);
testPointer.hover(point1);
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 300.0)));
await tester.pump();
expect(find.text('Test Title'), findsNothing);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
_verifySliverGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
// Scroll back to float in appbar
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, -30.0)));
await tester.pump();
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 56.0);
_verifySliverGeometry(key: appBarKey, paintExtent: 30.0, visible: true);
await tester.pumpAndSettle();
// The snap animation should have completed and the app bar should be
// fully expanded.
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 56.0);
_verifySliverGeometry(key: appBarKey, paintExtent: 56.0, visible: true);
// Float back out a bit and trigger snap close animation.
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 50.0)));
await tester.pump();
expect(find.text('Test Title'), findsOneWidget);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
expect(tester.renderObject<RenderBox>(find.byType(AppBar)).size.height, 56.0);
_verifySliverGeometry(key: appBarKey, paintExtent: 6.0, visible: true);
await tester.pumpAndSettle();
// The snap animation should have completed and the app bar should no
// longer be visible.
expect(find.text('Test Title'), findsNothing);
expect(find.text('Item 1'), findsNothing);
expect(find.text('Item 5'), findsOneWidget);
expect(find.byType(AppBar), findsNothing);
_verifySliverGeometry(key: appBarKey, paintExtent: 0.0, visible: false);
});
group('SliverAppBar - Stretch', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(stretch: true, expandedHeight: 100.0),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverScrollingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child!.size.height, equals(200.0));
});
testWidgets('fills overscroll after reverse direction input - scrolling header', (
WidgetTester tester,
) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(title: Text('Test'), stretch: true, expandedHeight: 100.0),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverScrollingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
expect(tester.getCenter(find.text('Test')).dy, 28.0);
// First scroll the header away
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(anchor)));
await gesture.moveBy(const Offset(0.0, -100.0));
await tester.pump(const Duration(milliseconds: 10));
expect(header.child!.size.height, equals(56.0));
expect(tester.getCenter(find.text('Test', skipOffstage: false)).dy, -28.0);
// With the same gesture, scroll back and into overscroll
await gesture.moveBy(const Offset(0.0, 200.0));
await tester.pump(const Duration(milliseconds: 10));
// Header should stretch in overscroll
expect(header.child!.size.height, equals(200.0));
expect(tester.getCenter(find.text('Test')).dy, 28.0);
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('fills overscroll after reverse direction input - floating header', (
WidgetTester tester,
) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
title: Text('Test'),
stretch: true,
floating: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverFloatingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
expect(tester.getCenter(find.text('Test')).dy, 28.0);
// First scroll the header away
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(anchor)));
await gesture.moveBy(const Offset(0.0, -100.0));
await tester.pump(const Duration(milliseconds: 10));
expect(header.child!.size.height, equals(56.0));
expect(tester.getCenter(find.text('Test', skipOffstage: false)).dy, -28.0);
// With the same gesture, scroll back and into overscroll
await gesture.moveBy(const Offset(0.0, 200.0));
await tester.pump(const Duration(milliseconds: 10));
// Header should stretch in overscroll
expect(header.child!.size.height, equals(200.0));
expect(tester.getCenter(find.text('Test')).dy, 28.0);
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(stretch: true, expandedHeight: 100.0),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverScrollingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100.0));
expect(header.child!.size.height, equals(100.0));
});
testWidgets('default trigger offset', (WidgetTester tester) async {
var didTrigger = false;
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
expandedHeight: 100.0,
onStretchTrigger: () async {
didTrigger = true;
},
),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
await slowDrag(tester, anchor, const Offset(0.0, 50.0));
expect(didTrigger, isFalse);
await tester.pumpAndSettle();
await slowDrag(tester, anchor, const Offset(0.0, 150.0));
expect(didTrigger, isTrue);
});
testWidgets('custom trigger offset', (WidgetTester tester) async {
var didTrigger = false;
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
expandedHeight: 100.0,
stretchTriggerOffset: 150.0,
onStretchTrigger: () async {
didTrigger = true;
},
),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
await slowDrag(tester, anchor, const Offset(0.0, 100.0));
await tester.pumpAndSettle();
expect(didTrigger, isFalse);
await slowDrag(tester, anchor, const Offset(0.0, 300.0));
expect(didTrigger, isTrue);
});
testWidgets('stretch callback not triggered without overscroll physics', (
WidgetTester tester,
) async {
var didTrigger = false;
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(
stretch: true,
expandedHeight: 100.0,
stretchTriggerOffset: 150.0,
onStretchTrigger: () async {
didTrigger = true;
},
),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
await slowDrag(tester, anchor, const Offset(0.0, 100.0));
await tester.pumpAndSettle();
expect(didTrigger, isFalse);
await slowDrag(tester, anchor, const Offset(0.0, 300.0));
expect(didTrigger, isFalse);
});
testWidgets('asserts reasonable trigger offset', (WidgetTester tester) async {
expect(() {
return MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
SliverAppBar(stretch: true, expandedHeight: 100.0, stretchTriggerOffset: -150.0),
SliverToBoxAdapter(child: Container(height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
);
}, throwsAssertionError);
});
});
group('SliverAppBar - Stretch, Pinned', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(pinned: true, stretch: true, expandedHeight: 100.0),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child!.size.height, equals(200.0));
});
testWidgets('does not stretch without overscroll physics', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(pinned: true, stretch: true, expandedHeight: 100.0),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child!.size.height, equals(100.0));
});
});
group('SliverAppBar - Stretch, Floating', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(floating: true, stretch: true, expandedHeight: 100.0),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverFloatingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child!.size.height, equals(200.0));
});
testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(floating: true, stretch: true, expandedHeight: 100.0),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverFloatingPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child!.size.height, equals(100.0));
});
});
group('SliverAppBar - Stretch, Floating, Pinned', () {
testWidgets('fills overscroll', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
floating: true,
pinned: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverFloatingPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child!.size.height, equals(200.0));
});
testWidgets('does not fill overscroll without proper physics', (WidgetTester tester) async {
const anchor = Key('drag');
await tester.pumpWidget(
MaterialApp(
home: CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: <Widget>[
const SliverAppBar(
pinned: true,
floating: true,
stretch: true,
expandedHeight: 100.0,
),
SliverToBoxAdapter(child: Container(key: anchor, height: 800)),
SliverToBoxAdapter(child: Container(height: 800)),
],
),
),
);
final RenderSliverFloatingPinnedPersistentHeader header = tester.renderObject(
find.byType(SliverAppBar),
);
expect(header.child!.size.height, equals(100.0));
await slowDrag(tester, anchor, const Offset(0.0, 100));
expect(header.child!.size.height, equals(100.0));
});
});
}