Unverified Commit e0417b5e authored by xubaolin's avatar xubaolin Committed by GitHub

RefreshIndicator should not be shown when overscroll occurs due to inertia (#72132)

parent e9dc2557
...@@ -47,6 +47,17 @@ enum _RefreshIndicatorMode { ...@@ -47,6 +47,17 @@ enum _RefreshIndicatorMode {
canceled, // Animating the indicator's fade-out after not arming. canceled, // Animating the indicator's fade-out after not arming.
} }
/// Used to configure how [RefreshIndicator] can be triggered.
enum RefreshIndicatorTriggerMode {
/// The indicator can be triggered regardless of the scroll position
/// of the [Scrollable] when the drag starts.
anywhere,
/// The indicator can only be triggered if the [Scrollable] is at the edge
/// when the drag starts.
onEdge,
}
/// A widget that supports the Material "swipe to refresh" idiom. /// A widget that supports the Material "swipe to refresh" idiom.
/// ///
/// When the child's [Scrollable] descendant overscrolls, an animated circular /// When the child's [Scrollable] descendant overscrolls, an animated circular
...@@ -56,6 +67,8 @@ enum _RefreshIndicatorMode { ...@@ -56,6 +67,8 @@ enum _RefreshIndicatorMode {
/// scrollable's contents and then complete the [Future] it returns. The refresh /// scrollable's contents and then complete the [Future] it returns. The refresh
/// indicator disappears after the callback's [Future] has completed. /// indicator disappears after the callback's [Future] has completed.
/// ///
/// The trigger mode is configured by [RefreshIndicator.triggerMode].
///
/// ## Troubleshooting /// ## Troubleshooting
/// ///
/// ### Refresh indicator does not show up /// ### Refresh indicator does not show up
...@@ -106,11 +119,13 @@ class RefreshIndicator extends StatefulWidget { ...@@ -106,11 +119,13 @@ class RefreshIndicator extends StatefulWidget {
this.notificationPredicate = defaultScrollNotificationPredicate, this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel, this.semanticsLabel,
this.semanticsValue, this.semanticsValue,
this.strokeWidth = 2.0 this.strokeWidth = 2.0,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : assert(child != null), }) : assert(child != null),
assert(onRefresh != null), assert(onRefresh != null),
assert(notificationPredicate != null), assert(notificationPredicate != null),
assert(strokeWidth != null), assert(strokeWidth != null),
assert(triggerMode != null),
super(key: key); super(key: key);
/// The widget below this widget in the tree. /// The widget below this widget in the tree.
...@@ -160,6 +175,21 @@ class RefreshIndicator extends StatefulWidget { ...@@ -160,6 +175,21 @@ class RefreshIndicator extends StatefulWidget {
/// By default, the value of `strokeWidth` is 2.0 pixels. /// By default, the value of `strokeWidth` is 2.0 pixels.
final double strokeWidth; final double strokeWidth;
/// Defines how this [RefreshIndicator] can be triggered when users overscroll.
///
/// The [RefreshIndicator] can be pulled out in two cases,
/// 1, Keep dragging if the scrollable widget at the edge with zero scroll position
/// when the drag starts.
/// 2, Keep dragging after overscroll occurs if the scrollable widget has
/// a non-zero scroll position when the drag starts.
///
/// If this is [RefreshIndicatorTriggerMode.anywhere], both of the cases above can be triggered.
///
/// If this is [RefreshIndicatorTriggerMode.onEdge], only case 1 can be triggered.
///
/// Defaults to [RefreshIndicatorTriggerMode.onEdge].
final RefreshIndicatorTriggerMode triggerMode;
@override @override
RefreshIndicatorState createState() => RefreshIndicatorState(); RefreshIndicatorState createState() => RefreshIndicatorState();
} }
...@@ -215,12 +245,17 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -215,12 +245,17 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
super.dispose(); super.dispose();
} }
bool _shouldStart(ScrollNotification notification) {
return (notification is ScrollStartNotification || (notification is ScrollUpdateNotification && notification.dragDetails != null && widget.triggerMode == RefreshIndicatorTriggerMode.anywhere))
&& notification.metrics.extentBefore == 0.0
&& _mode == null
&& _start(notification.metrics.axisDirection);
}
bool _handleScrollNotification(ScrollNotification notification) { bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification)) if (!widget.notificationPredicate(notification))
return false; return false;
if ((notification is ScrollStartNotification || notification is ScrollUpdateNotification) && if (_shouldStart(notification)) {
notification.metrics.extentBefore == 0.0 &&
_mode == null && _start(notification.metrics.axisDirection)) {
setState(() { setState(() {
_mode = _RefreshIndicatorMode.drag; _mode = _RefreshIndicatorMode.drag;
}); });
......
...@@ -501,12 +501,13 @@ void main() { ...@@ -501,12 +501,13 @@ void main() {
); );
}); });
testWidgets('Top RefreshIndicator showed when dragging from non-zero scroll position', (WidgetTester tester) async { testWidgets('Top RefreshIndicator(anywhere mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async {
refreshCalled = false; refreshCalled = false;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: RefreshIndicator( home: RefreshIndicator(
triggerMode: RefreshIndicatorTriggerMode.anywhere,
onRefresh: holdRefresh, onRefresh: holdRefresh,
child: ListView( child: ListView(
controller: scrollController, controller: scrollController,
...@@ -535,12 +536,13 @@ void main() { ...@@ -535,12 +536,13 @@ void main() {
expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0)); expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0));
}); });
testWidgets('Bottom RefreshIndicator showed when dragging from non-zero scroll position', (WidgetTester tester) async { testWidgets('Bottom RefreshIndicator(anywhere mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async {
refreshCalled = false; refreshCalled = false;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: RefreshIndicator( home: RefreshIndicator(
triggerMode: RefreshIndicatorTriggerMode.anywhere,
onRefresh: holdRefresh, onRefresh: holdRefresh,
child: ListView( child: ListView(
reverse: true, reverse: true,
...@@ -569,4 +571,110 @@ void main() { ...@@ -569,4 +571,110 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, greaterThan(300.0)); expect(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, greaterThan(300.0));
}); });
// Regression test for https://github.com/flutter/flutter/issues/71936
testWidgets('RefreshIndicator(anywhere mode) should not be shown when overscroll occurs due to inertia', (WidgetTester tester) async {
refreshCalled = false;
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: RefreshIndicator(
triggerMode: RefreshIndicatorTriggerMode.anywhere,
onRefresh: holdRefresh,
child: ListView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
children: const <Widget>[
SizedBox(
height: 200.0,
child: Text('X'),
),
SizedBox(
height: 2000.0,
child: Text('Y'),
),
],
),
),
),
);
scrollController.jumpTo(100.0);
// Release finger before reach the edge.
await tester.fling(find.text('X'), const Offset(0.0, 99.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(find.byType(RefreshProgressIndicator), findsNothing);
});
testWidgets('Top RefreshIndicator(onEdge mode) should not be shown when dragging from non-zero scroll position', (WidgetTester tester) async {
refreshCalled = false;
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: RefreshIndicator(
onRefresh: holdRefresh,
child: ListView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
children: const <Widget>[
SizedBox(
height: 200.0,
child: Text('X'),
),
SizedBox(
height: 800.0,
child: Text('Y'),
),
],
),
),
),
);
scrollController.jumpTo(50.0);
await tester.fling(find.text('X'), const Offset(0.0, 300.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(find.byType(RefreshProgressIndicator), findsNothing);
});
testWidgets('Bottom RefreshIndicator(onEdge mode) should be shown when dragging from non-zero scroll position', (WidgetTester tester) async {
refreshCalled = false;
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: RefreshIndicator(
onRefresh: holdRefresh,
child: ListView(
reverse: true,
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
children: const <Widget>[
SizedBox(
height: 200.0,
child: Text('X'),
),
SizedBox(
height: 800.0,
child: Text('Y'),
),
],
),
),
),
);
scrollController.jumpTo(50.0);
await tester.fling(find.text('X'), const Offset(0.0, -300.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
expect(find.byType(RefreshProgressIndicator), findsNothing);
});
} }
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