blob: f6dd3235d0f9e9c60320c45591dacd9c62f05bd9 [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/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
List<Widget> children(int n) {
return List<Widget>.generate(n, (int i) {
return SizedBox(height: 100.0, child: Text('$i'));
});
}
void main() {
testWidgets('Scrolling with list view changes, leaving the overscroll', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(MaterialApp(home: ListView(controller: controller, children: children(30))));
final double thirty = controller.position.maxScrollExtent;
controller.jumpTo(thirty);
await tester.pump();
controller.jumpTo(thirty + 100.0); // past the end
await tester.pump();
await tester.pumpWidget(MaterialApp(home: ListView(controller: controller, children: children(31))));
expect(controller.position.pixels, thirty + 100.0); // has the same position, but no longer overscrolled
expect(await tester.pumpAndSettle(), 1); // doesn't have ballistic animation...
expect(controller.position.pixels, thirty + 100.0); // and ends up at the end
});
testWidgets('Scrolling with list view changes, remaining overscrolled', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(MaterialApp(home: ListView(controller: controller, children: children(30))));
final double thirty = controller.position.maxScrollExtent;
controller.jumpTo(thirty);
await tester.pump();
controller.jumpTo(thirty + 200.0); // past the end
await tester.pump();
await tester.pumpWidget(MaterialApp(home: ListView(controller: controller, children: children(31))));
expect(controller.position.pixels, thirty + 200.0); // has the same position, still overscrolled
expect(await tester.pumpAndSettle(), 8); // now it goes ballistic...
expect(controller.position.pixels, thirty + 100.0); // and ends up at the end
});
testWidgets('Ability to keep a PageView at the end manually (issue 62209)', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(home: PageView62209()));
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 2'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 3'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 4'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 5'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsOneWidget);
await tester.tap(find.byType(TextButton)); // 6
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 6'), findsNothing);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsOneWidget);
await tester.tap(find.byType(TextButton)); // 7
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 6'), findsNothing);
expect(find.text('Page 7'), findsNothing);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 5'), findsOneWidget);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 4'), findsOneWidget);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsNothing);
await tester.tap(find.byType(TextButton)); // 8
await tester.pump();
expect(find.text('Page 1'), findsNothing);
expect(find.text('Page 8'), findsNothing);
expect(find.text('Page 4'), findsOneWidget);
expect(find.text('Page 5'), findsNothing);
expect(find.text('Page 100'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 3'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 2'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 6'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 7'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 8'), findsOneWidget);
await tester.drag(find.byType(PageView62209), const Offset(800.0, 0.0));
await tester.pump();
expect(find.text('Page 1'), findsOneWidget);
await tester.tap(find.byType(TextButton)); // 9
await tester.pump();
expect(find.text('Page 1'), findsOneWidget);
expect(find.text('Page 9'), findsNothing);
await tester.drag(find.byType(PageView62209), const Offset(-800.0, 0.0));
await tester.pump();
expect(find.text('Page 9'), findsOneWidget);
});
testWidgets('Pointer is not ignored during trackpad scrolling.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
int? lastTapped;
int? lastHovered;
await tester.pumpWidget(MaterialApp(
home: ListView(
controller: controller,
children: List<Widget>.generate(30, (int i) {
return SizedBox(height: 100.0, child: MouseRegion(
onHover: (PointerHoverEvent event) {
lastHovered = i;
},
child: GestureDetector(
onTap: () {
lastTapped = i;
},
child: Text('$i')
)
));
})
)
));
final TestGesture touchGesture = await tester.createGesture(kind: PointerDeviceKind.touch); // ignore: avoid_redundant_argument_values
// Try mouse hovering while scrolling by touch
await touchGesture.down(tester.getCenter(find.byType(ListView)));
await tester.pump();
await touchGesture.moveBy(const Offset(0, 200));
await tester.pump();
final TestGesture hoverGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await hoverGesture.addPointer(
location: tester.getCenter(find.text('3'))
);
await hoverGesture.moveBy(const Offset(1, 1));
await hoverGesture.removePointer(
location: tester.getCenter(find.text('3'))
);
await tester.pumpAndSettle();
expect(controller.position.activity?.shouldIgnorePointer, isTrue); // Pointer is ignored for touch scrolling.
expect(lastHovered, isNull);
await touchGesture.up();
await tester.pump();
// Try mouse clicking during inertia after scrolling by touch
await tester.fling(find.byType(ListView), const Offset(0, -200), 1000);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(controller.position.activity?.shouldIgnorePointer, isTrue); // Pointer is ignored following touch scrolling.
await tester.tap(find.text('3'), warnIfMissed: false);
expect(lastTapped, isNull);
await tester.pumpAndSettle();
controller.jumpTo(0);
await tester.pump();
final TestGesture trackpadGesture = await tester.createGesture(kind: PointerDeviceKind.trackpad);
// Try mouse hovering while scrolling with a trackpad
await trackpadGesture.panZoomStart(tester.getCenter(find.byType(ListView)));
await tester.pump();
await trackpadGesture.panZoomUpdate(tester.getCenter(find.byType(ListView)), pan: const Offset(0, 200));
await tester.pump();
await hoverGesture.addPointer(
location: tester.getCenter(find.text('3'))
);
await hoverGesture.moveBy(const Offset(1, 1));
await hoverGesture.removePointer(
location: tester.getCenter(find.text('3'))
);
await tester.pumpAndSettle();
expect(controller.position.activity?.shouldIgnorePointer, isFalse); // Pointer is not ignored for trackpad scrolling.
expect(lastHovered, equals(3));
await trackpadGesture.panZoomEnd();
await tester.pump();
// Try mouse clicking during inertia after scrolling with a trackpad
await tester.trackpadFling(find.byType(ListView), const Offset(0, -200), 1000);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(controller.position.activity?.shouldIgnorePointer, isFalse); // Pointer is not ignored following trackpad scrolling.
await tester.tap(find.text('3'));
expect(lastTapped, equals(3));
await tester.pumpAndSettle();
});
}
class PageView62209 extends StatefulWidget {
const PageView62209({super.key});
@override
State<PageView62209> createState() => _PageView62209State();
}
class _PageView62209State extends State<PageView62209> {
int _nextPageNum = 1;
final List<Carousel62209Page> _pages = <Carousel62209Page>[];
@override
void initState() {
super.initState();
for (int i = 0; i < 5; i++) {
_pages.add(Carousel62209Page(
key: Key('$_nextPageNum'),
number: _nextPageNum++,
));
}
_pages.add(const Carousel62209Page(number: 100));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(child: Carousel62209(pages: _pages)),
TextButton(
child: const Text('ADD PAGE'),
onPressed: () {
setState(() {
_pages.insert(
1,
Carousel62209Page(
key: Key('$_nextPageNum'),
number: _nextPageNum++,
),
);
});
},
),
],
),
);
}
}
class Carousel62209Page extends StatelessWidget {
const Carousel62209Page({required this.number, super.key});
final int number;
@override
Widget build(BuildContext context) {
return Center(child: Text('Page $number'));
}
}
class Carousel62209 extends StatefulWidget {
const Carousel62209({super.key, required this.pages});
final List<Carousel62209Page> pages;
@override
State<Carousel62209> createState() => _Carousel62209State();
}
class _Carousel62209State extends State<Carousel62209> {
// page variables
late PageController _pageController;
int _currentPage = 0;
// controls updates outside of user interaction
late List<Carousel62209Page> _pages;
bool _jumpingToPage = false;
@override
void initState() {
super.initState();
_pages = widget.pages.toList();
_pageController = PageController(keepPage: false);
}
@override
void didUpdateWidget(Carousel62209 oldWidget) {
super.didUpdateWidget(oldWidget);
if (!_jumpingToPage) {
int newPage = -1;
for (int i = 0; i < widget.pages.length; i++) {
if (widget.pages[i].number == _pages[_currentPage].number) {
newPage = i;
}
}
if (newPage == _currentPage) {
_pages = widget.pages.toList();
} else {
_jumpingToPage = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_pages = widget.pages.toList();
_currentPage = newPage;
_pageController.jumpToPage(_currentPage);
SchedulerBinding.instance.addPostFrameCallback((_) {
_jumpingToPage = false;
});
});
}
});
}
}
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification) {
final int page = _pageController.page!.round();
if (!_jumpingToPage && _currentPage != page) {
_currentPage = page;
}
}
return true;
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification,
child: PageView.builder(
controller: _pageController,
itemCount: _pages.length,
itemBuilder: (BuildContext context, int index) {
return _pages[index];
},
),
);
}
}