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 {
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.
///
/// When the child's [Scrollable] descendant overscrolls, an animated circular
......@@ -56,6 +67,8 @@ enum _RefreshIndicatorMode {
/// scrollable's contents and then complete the [Future] it returns. The refresh
/// indicator disappears after the callback's [Future] has completed.
///
/// The trigger mode is configured by [RefreshIndicator.triggerMode].
///
/// ## Troubleshooting
///
/// ### Refresh indicator does not show up
......@@ -106,11 +119,13 @@ class RefreshIndicator extends StatefulWidget {
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = 2.0
this.strokeWidth = 2.0,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : assert(child != null),
assert(onRefresh != null),
assert(notificationPredicate != null),
assert(strokeWidth != null),
assert(triggerMode != null),
super(key: key);
/// The widget below this widget in the tree.
......@@ -160,6 +175,21 @@ class RefreshIndicator extends StatefulWidget {
/// By default, the value of `strokeWidth` is 2.0 pixels.
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
RefreshIndicatorState createState() => RefreshIndicatorState();
}
......@@ -215,12 +245,17 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
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) {
if (!widget.notificationPredicate(notification))
return false;
if ((notification is ScrollStartNotification || notification is ScrollUpdateNotification) &&
notification.metrics.extentBefore == 0.0 &&
_mode == null && _start(notification.metrics.axisDirection)) {
if (_shouldStart(notification)) {
setState(() {
_mode = _RefreshIndicatorMode.drag;
});
......
......@@ -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;
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: RefreshIndicator(
triggerMode: RefreshIndicatorTriggerMode.anywhere,
onRefresh: holdRefresh,
child: ListView(
controller: scrollController,
......@@ -535,12 +536,13 @@ void main() {
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;
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: RefreshIndicator(
triggerMode: RefreshIndicatorTriggerMode.anywhere,
onRefresh: holdRefresh,
child: ListView(
reverse: true,
......@@ -569,4 +571,110 @@ void main() {
await tester.pump(const Duration(seconds: 1)); // finish the indicator settle animation
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