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 {
}
}
/// 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);
......
......@@ -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() {
......
......@@ -35,4 +35,36 @@ void main() {
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() {
new RefreshIndicator(
onRefresh: refresh,
child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <String>['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: <Widget>[
new SizedBox(
height: 200.0,
......@@ -75,6 +77,7 @@ void main() {
new RefreshIndicator(
onRefresh: holdRefresh,
child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
new SizedBox(
height: 200.0,
......@@ -99,6 +102,7 @@ void main() {
onRefresh: holdRefresh,
child: new ListView(
reverse: true,
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
new SizedBox(
height: 200.0,
......@@ -122,6 +126,7 @@ void main() {
new RefreshIndicator(
onRefresh: refresh,
child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
new SizedBox(
height: 200.0,
......@@ -147,6 +152,7 @@ void main() {
new RefreshIndicator(
onRefresh: refresh,
child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
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: <Widget>[
new SizedBox(
height: 200.0,
......@@ -211,6 +218,7 @@ void main() {
new RefreshIndicator(
onRefresh: refresh,
child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
new SizedBox(
height: 200.0,
......@@ -252,6 +260,7 @@ void main() {
new RefreshIndicator(
onRefresh: refresh,
child: new ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: <Widget>[
new SizedBox(
height: 200.0,
......
......@@ -126,6 +126,7 @@ void main() {
testWidgets('down', (WidgetTester tester) async {
await tester.pumpWidget(
new CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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