diff --git a/examples/flutter_gallery/lib/demo/material/list_demo.dart b/examples/flutter_gallery/lib/demo/material/list_demo.dart index 14308302bb2..163985c043d 100644 --- a/examples/flutter_gallery/lib/demo/material/list_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/list_demo.dart @@ -42,6 +42,7 @@ class _ListDemoState extends State { ), child: new ListView( shrinkWrap: true, + primary: false, children: [ new ListTile( dense: true, diff --git a/examples/flutter_gallery/lib/gallery/drawer.dart b/examples/flutter_gallery/lib/gallery/drawer.dart index 92f765b6d76..584292401db 100644 --- a/examples/flutter_gallery/lib/gallery/drawer.dart +++ b/examples/flutter_gallery/lib/gallery/drawer.dart @@ -298,6 +298,6 @@ class GalleryDrawer extends StatelessWidget { )); } - return new Drawer(child: new ListView(children: allDrawerItems)); + return new Drawer(child: new ListView(primary: false, children: allDrawerItems)); } } diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index 6806c0fb9fa..8febea5cbbd 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -215,7 +215,7 @@ class _PagePosition extends ScrollPositionWithSingleContext { /// These physics cause the page view to snap to page boundaries. class PageScrollPhysics extends ScrollPhysics { /// Creates physics for a [PageView]. - const PageScrollPhysics({ ScrollPhysics parent }) : super(parent); + const PageScrollPhysics({ ScrollPhysics parent }) : super(parent: parent); @override PageScrollPhysics applyTo(ScrollPhysics parent) => new PageScrollPhysics(parent: parent); diff --git a/packages/flutter/lib/src/widgets/scroll_metrics.dart b/packages/flutter/lib/src/widgets/scroll_metrics.dart index 376243c52ad..eec43e0d008 100644 --- a/packages/flutter/lib/src/widgets/scroll_metrics.dart +++ b/packages/flutter/lib/src/widgets/scroll_metrics.dart @@ -88,11 +88,6 @@ abstract class ScrollMetrics { /// of the viewport in the scrollable. This is the content below the content /// described by [extentInside]. double get extentAfter => math.max(maxScrollExtent - pixels, 0.0); - - @override - String toString() { - return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)}})'; - } } /// An immutable snapshot of values associated with a [Scrollable] viewport. @@ -131,4 +126,9 @@ class FixedScrollMetrics extends ScrollMetrics { @override final AxisDirection axisDirection; + + @override + String toString() { + return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)})'; + } } \ No newline at end of file diff --git a/packages/flutter/lib/src/widgets/scroll_physics.dart b/packages/flutter/lib/src/widgets/scroll_physics.dart index 35f58950cd7..9af5b576444 100644 --- a/packages/flutter/lib/src/widgets/scroll_physics.dart +++ b/packages/flutter/lib/src/widgets/scroll_physics.dart @@ -16,12 +16,12 @@ import 'scroll_simulation.dart'; export 'package:flutter/physics.dart' show Tolerance; @immutable -abstract class ScrollPhysics { - const ScrollPhysics(this.parent); +class ScrollPhysics { + const ScrollPhysics({ this.parent }); final ScrollPhysics parent; - ScrollPhysics applyTo(ScrollPhysics parent); + ScrollPhysics applyTo(ScrollPhysics parent) => new ScrollPhysics(parent: parent); /// Used by [DragScrollActivity] and other user-driven activities to /// convert an offset in logical pixels as provided by the [DragUpdateDetails] @@ -172,7 +172,7 @@ abstract class ScrollPhysics { /// clamping behavior. class BouncingScrollPhysics extends ScrollPhysics { /// Creates scroll physics that bounce back from the edge. - const BouncingScrollPhysics({ ScrollPhysics parent }) : super(parent); + const BouncingScrollPhysics({ ScrollPhysics parent }) : super(parent: parent); @override BouncingScrollPhysics applyTo(ScrollPhysics parent) => new BouncingScrollPhysics(parent: parent); @@ -251,7 +251,7 @@ class BouncingScrollPhysics extends ScrollPhysics { class ClampingScrollPhysics extends ScrollPhysics { /// Creates scroll physics that prevent the scroll offset from exceeding the /// bounds of the content.. - const ClampingScrollPhysics({ ScrollPhysics parent }) : super(parent); + const ClampingScrollPhysics({ ScrollPhysics parent }) : super(parent: parent); @override ClampingScrollPhysics applyTo(ScrollPhysics parent) => new ClampingScrollPhysics(parent: parent); @@ -325,13 +325,15 @@ class ClampingScrollPhysics extends ScrollPhysics { /// /// See also: /// +/// * [ScrollPhysics], which can be used instead of this class when the default +/// behavior is desired instead. /// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior /// found on iOS. /// * [ClampingScrollPhysics], which provides the clamping overscroll behavior /// found on Android. class AlwaysScrollableScrollPhysics extends ScrollPhysics { /// Creates scroll physics that always lets the user scroll. - const AlwaysScrollableScrollPhysics({ ScrollPhysics parent }) : super(parent); + const AlwaysScrollableScrollPhysics({ ScrollPhysics parent }) : super(parent: parent); @override AlwaysScrollableScrollPhysics applyTo(ScrollPhysics parent) => new AlwaysScrollableScrollPhysics(parent: parent); diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart index e3f10a1497b..459bda5327f 100644 --- a/packages/flutter/lib/src/widgets/scroll_view.dart +++ b/packages/flutter/lib/src/widgets/scroll_view.dart @@ -49,9 +49,10 @@ abstract class ScrollView extends StatelessWidget { this.reverse: false, this.controller, bool primary, - this.physics, + ScrollPhysics physics, this.shrinkWrap: false, }) : primary = primary ?? controller == null && scrollDirection == Axis.vertical, + physics = physics ?? (primary == true || (primary == null && controller == null && scrollDirection == Axis.vertical) ? const AlwaysScrollableScrollPhysics() : null), super(key: key) { assert(reverse != null); assert(shrinkWrap != null); @@ -90,7 +91,11 @@ abstract class ScrollView extends StatelessWidget { /// Whether this is the primary scroll view associated with the parent /// [PrimaryScrollController]. /// - /// On iOS, this identifies the scroll view that will scroll to top in + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in /// response to a tap in the status bar. /// /// Defaults to true when [scrollDirection] is [Axis.vertical] and @@ -102,7 +107,26 @@ abstract class ScrollView extends StatelessWidget { /// For example, determines how the scroll view continues to animate after the /// user stops dragging the scroll view. /// - /// Defaults to matching platform conventions. + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` final ScrollPhysics physics; /// Whether the extent of the scroll view in the [scrollDirection] should be diff --git a/packages/flutter/test/material/persistent_bottom_sheet_test.dart b/packages/flutter/test/material/persistent_bottom_sheet_test.dart index b53159cec74..0d8ee8441d8 100644 --- a/packages/flutter/test/material/persistent_bottom_sheet_test.dart +++ b/packages/flutter/test/material/persistent_bottom_sheet_test.dart @@ -48,6 +48,7 @@ void main() { scaffoldKey.currentState.showBottomSheet((BuildContext context) { return new ListView( shrinkWrap: true, + primary: false, children: [ new Container(height: 100.0, child: const Text('One')), new Container(height: 100.0, child: const Text('Two')), diff --git a/packages/flutter/test/widgets/scroll_view_test.dart b/packages/flutter/test/widgets/scroll_view_test.dart index 54c841077f8..84cbc0a2847 100644 --- a/packages/flutter/test/widgets/scroll_view_test.dart +++ b/packages/flutter/test/widgets/scroll_view_test.dart @@ -278,4 +278,86 @@ void main() { ); expect(innerScrollable.controller, isNull); }); + + testWidgets('Primary ListViews are always scrollable', (WidgetTester tester) async { + final ListView view = new ListView(primary: true); + expect(view.physics, const isInstanceOf()); + }); + + testWidgets('Non-primary ListViews are not always scrollable', (WidgetTester tester) async { + final ListView view = new ListView(primary: false); + expect(view.physics, isNot(const isInstanceOf())); + }); + + testWidgets('Defaulting-to-primary ListViews are always scrollable', (WidgetTester tester) async { + final ListView view = new ListView(scrollDirection: Axis.vertical); + expect(view.physics, const isInstanceOf()); + }); + + testWidgets('Defaulting-to-not-primary ListViews are not always scrollable', (WidgetTester tester) async { + final ListView view = new ListView(scrollDirection: Axis.horizontal); + expect(view.physics, isNot(const isInstanceOf())); + }); + + testWidgets('primary:true leads to scrolling', (WidgetTester tester) async { + bool scrolled = false; + await tester.pumpWidget( + new NotificationListener( + onNotification: (OverscrollNotification message) { scrolled = true; return false; }, + child: new ListView( + primary: true, + children: [], + ), + ), + ); + await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); + expect(scrolled, isTrue); + }); + + testWidgets('primary:false leads to no scrolling', (WidgetTester tester) async { + bool scrolled = false; + await tester.pumpWidget( + new NotificationListener( + onNotification: (OverscrollNotification message) { scrolled = true; return false; }, + child: new ListView( + primary: false, + children: [], + ), + ), + ); + await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); + expect(scrolled, isFalse); + }); + + testWidgets('physics:AlwaysScrollableScrollPhysics actually overrides primary:false default behaviour', (WidgetTester tester) async { + bool scrolled = false; + await tester.pumpWidget( + new NotificationListener( + onNotification: (OverscrollNotification message) { scrolled = true; return false; }, + child: new ListView( + primary: false, + physics: const AlwaysScrollableScrollPhysics(), + children: [], + ), + ), + ); + await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); + expect(scrolled, isTrue); + }); + + testWidgets('physics:ScrollPhysics actually overrides primary:true default behaviour', (WidgetTester tester) async { + bool scrolled = false; + await tester.pumpWidget( + new NotificationListener( + onNotification: (OverscrollNotification message) { scrolled = true; return false; }, + child: new ListView( + primary: true, + physics: const ScrollPhysics(), + children: [], + ), + ), + ); + await tester.dragFrom(const Offset(100.0, 100.0), const Offset(0.0, 100.0)); + expect(scrolled, isFalse); + }); }