Commit 0e8b6aab authored by Adam Barth's avatar Adam Barth Committed by GitHub

ListView shouldn't be scrollable when there isn't any scroll extent (#8318)

Previously, a ListView would always accept user input, even if it wasn't
actually scrollable. Now, by default, we don't accept user input if there's no
scroll range. You can override this behavior using the ScrollPhysics.

Fixes #8276
Fixes #8278
Fixes #8271
parent 7db8241a
...@@ -145,6 +145,28 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -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 { class PageScrollPhysics extends ScrollPhysics {
const PageScrollPhysics({ ScrollPhysics parent }) : super(parent); const PageScrollPhysics({ ScrollPhysics parent }) : super(parent);
......
...@@ -51,6 +51,17 @@ abstract class ScrollPhysics { ...@@ -51,6 +51,17 @@ abstract class ScrollPhysics {
return parent.applyPhysicsToUserOffset(position, offset); 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. /// Determines the overscroll by applying the boundary conditions.
/// ///
/// Called by [ScrollPosition.setPixels] just before the [pixels] value is /// Called by [ScrollPosition.setPixels] just before the [pixels] value is
...@@ -329,7 +340,6 @@ class ScrollPosition extends ViewportOffset { ...@@ -329,7 +340,6 @@ class ScrollPosition extends ViewportOffset {
// soon afterwards in the same layout phase. So we put all the logic that // soon afterwards in the same layout phase. So we put all the logic that
// relies on both values being computed into applyContentDimensions. // relies on both values being computed into applyContentDimensions.
} }
state.setCanDrag(canDrag);
return true; return true;
} }
...@@ -343,7 +353,7 @@ class ScrollPosition extends ViewportOffset { ...@@ -343,7 +353,7 @@ class ScrollPosition extends ViewportOffset {
activity.applyNewDimensions(); activity.applyNewDimensions();
_didChangeViewportDimension = false; _didChangeViewportDimension = false;
} }
state.setCanDrag(canDrag); state.setCanDrag(physics.shouldAcceptUserOffset(this));
return true; return true;
} }
...@@ -392,8 +402,6 @@ class ScrollPosition extends ViewportOffset { ...@@ -392,8 +402,6 @@ class ScrollPosition extends ViewportOffset {
activity.resetActivity(); activity.resetActivity();
} }
bool get canDrag => true;
bool get shouldIgnorePointer => activity?.shouldIgnorePointer; bool get shouldIgnorePointer => activity?.shouldIgnorePointer;
void touched() { void touched() {
......
...@@ -35,4 +35,36 @@ void main() { ...@@ -35,4 +35,36 @@ void main() {
expect(buildCount, equals(2)); expect(buildCount, equals(2));
}); });
testWidgets('Verify that a scrollable BottomSheet can be dismissed', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
key: scaffoldKey,
body: new Center(child: new Text('body'))
)
));
scaffoldKey.currentState.showBottomSheet<Null>((BuildContext context) {
return new ListView(
shrinkWrap: true,
children: <Widget>[
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);
});
} }
...@@ -26,6 +26,7 @@ void main() { ...@@ -26,6 +26,7 @@ void main() {
new RefreshIndicator( new RefreshIndicator(
onRefresh: refresh, onRefresh: refresh,
child: new ListView( child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map((String item) { children: <String>['A', 'B', 'C', 'D', 'E', 'F'].map((String item) {
return new SizedBox( return new SizedBox(
height: 200.0, height: 200.0,
...@@ -51,6 +52,7 @@ void main() { ...@@ -51,6 +52,7 @@ void main() {
onRefresh: refresh, onRefresh: refresh,
child: new ListView( child: new ListView(
reverse: true, reverse: true,
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
...@@ -75,6 +77,7 @@ void main() { ...@@ -75,6 +77,7 @@ void main() {
new RefreshIndicator( new RefreshIndicator(
onRefresh: holdRefresh, onRefresh: holdRefresh,
child: new ListView( child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
...@@ -99,6 +102,7 @@ void main() { ...@@ -99,6 +102,7 @@ void main() {
onRefresh: holdRefresh, onRefresh: holdRefresh,
child: new ListView( child: new ListView(
reverse: true, reverse: true,
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
...@@ -122,6 +126,7 @@ void main() { ...@@ -122,6 +126,7 @@ void main() {
new RefreshIndicator( new RefreshIndicator(
onRefresh: refresh, onRefresh: refresh,
child: new ListView( child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
...@@ -147,6 +152,7 @@ void main() { ...@@ -147,6 +152,7 @@ void main() {
new RefreshIndicator( new RefreshIndicator(
onRefresh: refresh, onRefresh: refresh,
child: new ListView( child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
...@@ -171,6 +177,7 @@ void main() { ...@@ -171,6 +177,7 @@ void main() {
new RefreshIndicator( new RefreshIndicator(
onRefresh: holdRefresh, // this one never returns onRefresh: holdRefresh, // this one never returns
child: new ListView( child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
...@@ -211,6 +218,7 @@ void main() { ...@@ -211,6 +218,7 @@ void main() {
new RefreshIndicator( new RefreshIndicator(
onRefresh: refresh, onRefresh: refresh,
child: new ListView( child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
...@@ -252,6 +260,7 @@ void main() { ...@@ -252,6 +260,7 @@ void main() {
new RefreshIndicator( new RefreshIndicator(
onRefresh: refresh, onRefresh: refresh,
child: new ListView( child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[ children: <Widget>[
new SizedBox( new SizedBox(
height: 200.0, height: 200.0,
......
...@@ -126,6 +126,7 @@ void main() { ...@@ -126,6 +126,7 @@ void main() {
testWidgets('down', (WidgetTester tester) async { testWidgets('down', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new CustomScrollView( new CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), new SliverToBoxAdapter(child: const SizedBox(height: 20.0)),
], ],
...@@ -143,6 +144,7 @@ void main() { ...@@ -143,6 +144,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new CustomScrollView( new CustomScrollView(
reverse: true, reverse: true,
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), new SliverToBoxAdapter(child: const SizedBox(height: 20.0)),
], ],
...@@ -160,6 +162,7 @@ void main() { ...@@ -160,6 +162,7 @@ void main() {
testWidgets('Overscroll in both directions', (WidgetTester tester) async { testWidgets('Overscroll in both directions', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new CustomScrollView( new CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), new SliverToBoxAdapter(child: const SizedBox(height: 20.0)),
], ],
...@@ -180,6 +183,7 @@ void main() { ...@@ -180,6 +183,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
new CustomScrollView( new CustomScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), new SliverToBoxAdapter(child: const SizedBox(height: 20.0)),
], ],
...@@ -228,6 +232,7 @@ void main() { ...@@ -228,6 +232,7 @@ void main() {
behavior: new TestScrollBehavior1(), behavior: new TestScrollBehavior1(),
child: new CustomScrollView( child: new CustomScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics(),
reverse: true, reverse: true,
slivers: <Widget>[ slivers: <Widget>[
new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), new SliverToBoxAdapter(child: const SizedBox(height: 20.0)),
...@@ -246,6 +251,7 @@ void main() { ...@@ -246,6 +251,7 @@ void main() {
behavior: new TestScrollBehavior2(), behavior: new TestScrollBehavior2(),
child: new CustomScrollView( child: new CustomScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[ slivers: <Widget>[
new SliverToBoxAdapter(child: const SizedBox(height: 20.0)), new SliverToBoxAdapter(child: const SizedBox(height: 20.0)),
], ],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment