diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 37f3753313..5e3a8b34d7 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -344,6 +344,7 @@ class PageView extends StatefulWidget { this.reverse: false, PageController controller, this.physics, + this.pageSnapping: true, this.onPageChanged, List children: const [], }) : controller = controller ?? _defaultPageController, @@ -368,6 +369,7 @@ class PageView extends StatefulWidget { this.reverse: false, PageController controller, this.physics, + this.pageSnapping: true, this.onPageChanged, @required IndexedWidgetBuilder itemBuilder, int itemCount, @@ -383,6 +385,7 @@ class PageView extends StatefulWidget { this.reverse: false, PageController controller, this.physics, + this.pageSnapping: true, this.onPageChanged, @required this.childrenDelegate, }) : assert(childrenDelegate != null), @@ -423,6 +426,9 @@ class PageView extends StatefulWidget { /// Defaults to matching platform conventions. final ScrollPhysics physics; + /// Set to false to disable page snapping, useful for custom scroll behavior. + final bool pageSnapping; + /// Called whenever the page in the center of the viewport changes. final ValueChanged onPageChanged; @@ -463,6 +469,10 @@ class _PageViewState extends State { @override Widget build(BuildContext context) { final AxisDirection axisDirection = _getDirection(context); + final ScrollPhysics physics = widget.pageSnapping + ? _kPagePhysics.applyTo(widget.physics) + : widget.physics; + return new NotificationListener( onNotification: (ScrollNotification notification) { if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) { @@ -478,7 +488,7 @@ class _PageViewState extends State { child: new Scrollable( axisDirection: axisDirection, controller: widget.controller, - physics: widget.physics == null ? _kPagePhysics : _kPagePhysics.applyTo(widget.physics), + physics: physics, viewportBuilder: (BuildContext context, ViewportOffset position) { return new Viewport( axisDirection: axisDirection, @@ -502,5 +512,6 @@ class _PageViewState extends State { description.add(new FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed')); description.add(new DiagnosticsProperty('controller', widget.controller, showName: false)); description.add(new DiagnosticsProperty('physics', widget.physics, showName: false)); + description.add(new FlagProperty('pageSnapping', value: widget.pageSnapping, ifFalse: 'snapping disabled')); } } diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 7cd33729ce..48f3dbc239 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -368,6 +368,69 @@ void main() { expect(tester.getTopLeft(find.text('Idaho')), const Offset(790.0, 0.0)); }); + testWidgets('Page snapping disable and reenable', (WidgetTester tester) async { + final List log = []; + + Widget build({ bool pageSnapping }) { + return new Directionality( + textDirection: TextDirection.ltr, + child: new PageView( + pageSnapping: pageSnapping, + onPageChanged: log.add, + children: + kStates.map((String state) => new Text(state)).toList(), + ), + ); + } + + await tester.pumpWidget(build(pageSnapping: true)); + expect(log, isEmpty); + + // Drag more than halfway to the next page, to confirm the default behavior. + TestGesture gesture = await tester.startGesture(const Offset(100.0, 100.0)); + // The page view is 800.0 wide, so this move is just beyond halfway. + await gesture.moveBy(const Offset(-420.0, 0.0)); + + expect(log, equals(const [1])); + log.clear(); + + // Release the gesture, confirm that the page settles on the next. + await gesture.up(); + await tester.pumpAndSettle(); + + expect(find.text('Alabama'), findsNothing); + expect(find.text('Alaska'), findsOneWidget); + + // Disable page snapping, and try moving halfway. Confirm it doesn't snap. + await tester.pumpWidget(build(pageSnapping: false)); + gesture = await tester.startGesture(const Offset(100.0, 100.0)); + // Move just beyond halfway, again. + await gesture.moveBy(const Offset(-420.0, 0.0)); + + // Page notifications still get sent. + expect(log, equals(const [2])); + log.clear(); + + // Release the gesture, confirm that both pages are visible. + await gesture.up(); + await tester.pumpAndSettle(); + + expect(find.text('Alabama'), findsNothing); + expect(find.text('Alaska'), findsOneWidget); + expect(find.text('Arizona'), findsOneWidget); + expect(find.text('Arkansas'), findsNothing); + + // Now re-enable snapping, confirm that we've settled on a page. + await tester.pumpWidget(build(pageSnapping: true)); + await tester.pumpAndSettle(); + + expect(log, isEmpty); + + expect(find.text('Alaska'), findsNothing); + expect(find.text('Arizona'), findsOneWidget); + expect(find.text('Arkansas'), findsNothing); + }); + testWidgets('PageView small viewportFraction', (WidgetTester tester) async { final PageController controller = new PageController(viewportFraction: 1/8);