diff --git a/packages/flutter/lib/src/widgets/scroll_physics.dart b/packages/flutter/lib/src/widgets/scroll_physics.dart index 3a19d524a1..94a9ebc3f5 100644 --- a/packages/flutter/lib/src/widgets/scroll_physics.dart +++ b/packages/flutter/lib/src/widgets/scroll_physics.dart @@ -145,6 +145,28 @@ class ClampingScrollPhysics extends ScrollPhysics { } } +/// Scroll physics that always lets the user scroll. +/// +/// On Android, overscrolls will be clamped by default and result in an +/// overscroll glow. On iOS, overscrolls will load a spring that will return +/// the scroll view to its normal range when released. +/// +/// See also: +/// +/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior +/// found on iOS. +/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior +/// found on Android. +class AlwaysScrollableScrollPhysics extends ScrollPhysics { + const AlwaysScrollableScrollPhysics({ ScrollPhysics parent }) : super(parent); + + @override + AlwaysScrollableScrollPhysics applyTo(ScrollPhysics parent) => new AlwaysScrollableScrollPhysics(parent: parent); + + @override + bool shouldAcceptUserOffset(ScrollPosition position) => true; +} + class PageScrollPhysics extends ScrollPhysics { const PageScrollPhysics({ ScrollPhysics parent }) : super(parent); diff --git a/packages/flutter/lib/src/widgets/scroll_position.dart b/packages/flutter/lib/src/widgets/scroll_position.dart index 3a307dfdd9..706bb23ba1 100644 --- a/packages/flutter/lib/src/widgets/scroll_position.dart +++ b/packages/flutter/lib/src/widgets/scroll_position.dart @@ -51,6 +51,17 @@ abstract class ScrollPhysics { return parent.applyPhysicsToUserOffset(position, offset); } + /// Whether the scrollable should let the user adjust the scroll offset, for + /// example by dragging. + /// + /// By default, the user can manipulate the scroll offset if, and only if, + /// there is actually content outside the viewport to reveal. + bool shouldAcceptUserOffset(ScrollPosition position) { + if (parent == null) + return position.minScrollExtent != position.maxScrollExtent; + return parent.shouldAcceptUserOffset(position); + } + /// Determines the overscroll by applying the boundary conditions. /// /// Called by [ScrollPosition.setPixels] just before the [pixels] value is @@ -329,7 +340,6 @@ class ScrollPosition extends ViewportOffset { // soon afterwards in the same layout phase. So we put all the logic that // relies on both values being computed into applyContentDimensions. } - state.setCanDrag(canDrag); return true; } @@ -343,7 +353,7 @@ class ScrollPosition extends ViewportOffset { activity.applyNewDimensions(); _didChangeViewportDimension = false; } - state.setCanDrag(canDrag); + state.setCanDrag(physics.shouldAcceptUserOffset(this)); return true; } @@ -392,8 +402,6 @@ class ScrollPosition extends ViewportOffset { activity.resetActivity(); } - bool get canDrag => true; - bool get shouldIgnorePointer => activity?.shouldIgnorePointer; void touched() { diff --git a/packages/flutter/test/widgets/bottom_sheet_test.dart b/packages/flutter/test/material/modal_bottom_sheet_test.dart similarity index 100% rename from packages/flutter/test/widgets/bottom_sheet_test.dart rename to packages/flutter/test/material/modal_bottom_sheet_test.dart diff --git a/packages/flutter/test/widgets/bottom_sheet_rebuild_test.dart b/packages/flutter/test/material/persistent_bottom_sheet_test.dart similarity index 52% rename from packages/flutter/test/widgets/bottom_sheet_rebuild_test.dart rename to packages/flutter/test/material/persistent_bottom_sheet_test.dart index eaf8e3c1da..b3efb11e34 100644 --- a/packages/flutter/test/widgets/bottom_sheet_rebuild_test.dart +++ b/packages/flutter/test/material/persistent_bottom_sheet_test.dart @@ -35,4 +35,36 @@ void main() { expect(buildCount, equals(2)); }); + testWidgets('Verify that a scrollable BottomSheet can be dismissed', (WidgetTester tester) async { + final GlobalKey scaffoldKey = new GlobalKey(); + + await tester.pumpWidget(new MaterialApp( + home: new Scaffold( + key: scaffoldKey, + body: new Center(child: new Text('body')) + ) + )); + + scaffoldKey.currentState.showBottomSheet((BuildContext context) { + return new ListView( + shrinkWrap: true, + children: [ + new Container(height: 100.0, child: new Text('One')), + new Container(height: 100.0, child: new Text('Two')), + new Container(height: 100.0, child: new Text('Three')), + ], + ); + }); + + await tester.pumpUntilNoTransientCallbacks(); + + expect(find.text('Two'), findsOneWidget); + + await tester.scroll(find.text('Two'), const Offset(0.0, 400.0)); + await tester.pump(); + await tester.pumpUntilNoTransientCallbacks(); + + expect(find.text('Two'), findsNothing); + }); + } diff --git a/packages/flutter/test/material/refresh_indicator_test.dart b/packages/flutter/test/material/refresh_indicator_test.dart index a14e716b90..2c9a3fdd73 100644 --- a/packages/flutter/test/material/refresh_indicator_test.dart +++ b/packages/flutter/test/material/refresh_indicator_test.dart @@ -26,6 +26,7 @@ void main() { new RefreshIndicator( onRefresh: refresh, child: new ListView( + physics: const AlwaysScrollableScrollPhysics(), children: ['A', 'B', 'C', 'D', 'E', 'F'].map((String item) { return new SizedBox( height: 200.0, @@ -51,6 +52,7 @@ void main() { onRefresh: refresh, child: new ListView( reverse: true, + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, @@ -75,6 +77,7 @@ void main() { new RefreshIndicator( onRefresh: holdRefresh, child: new ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, @@ -99,6 +102,7 @@ void main() { onRefresh: holdRefresh, child: new ListView( reverse: true, + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, @@ -122,6 +126,7 @@ void main() { new RefreshIndicator( onRefresh: refresh, child: new ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, @@ -147,6 +152,7 @@ void main() { new RefreshIndicator( onRefresh: refresh, child: new ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, @@ -171,6 +177,7 @@ void main() { new RefreshIndicator( onRefresh: holdRefresh, // this one never returns child: new ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, @@ -211,6 +218,7 @@ void main() { new RefreshIndicator( onRefresh: refresh, child: new ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, @@ -252,6 +260,7 @@ void main() { new RefreshIndicator( onRefresh: refresh, child: new ListView( + physics: const AlwaysScrollableScrollPhysics(), children: [ new SizedBox( height: 200.0, diff --git a/packages/flutter/test/widgets/overscroll_indicator_test.dart b/packages/flutter/test/widgets/overscroll_indicator_test.dart index c479a56258..055d68c5e0 100644 --- a/packages/flutter/test/widgets/overscroll_indicator_test.dart +++ b/packages/flutter/test/widgets/overscroll_indicator_test.dart @@ -126,6 +126,7 @@ void main() { testWidgets('down', (WidgetTester tester) async { await tester.pumpWidget( new CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), slivers: [ new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), ], @@ -143,6 +144,7 @@ void main() { await tester.pumpWidget( new CustomScrollView( reverse: true, + physics: const AlwaysScrollableScrollPhysics(), slivers: [ new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), ], @@ -160,6 +162,7 @@ void main() { testWidgets('Overscroll in both directions', (WidgetTester tester) async { await tester.pumpWidget( new CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), slivers: [ new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), ], @@ -180,6 +183,7 @@ void main() { await tester.pumpWidget( new CustomScrollView( scrollDirection: Axis.horizontal, + physics: const AlwaysScrollableScrollPhysics(), slivers: [ new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), ], @@ -228,6 +232,7 @@ void main() { behavior: new TestScrollBehavior1(), child: new CustomScrollView( scrollDirection: Axis.horizontal, + physics: const AlwaysScrollableScrollPhysics(), reverse: true, slivers: [ new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), @@ -246,6 +251,7 @@ void main() { behavior: new TestScrollBehavior2(), child: new CustomScrollView( scrollDirection: Axis.horizontal, + physics: const AlwaysScrollableScrollPhysics(), slivers: [ new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), ],