mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
RefreshIndicator should not be shown when overscroll occurs due to inertia (#72132)
This commit is contained in:
parent
e9dc2557c9
commit
e0417b5e0f
@ -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 {
|
|||||||
/// 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 {
|
|||||||
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 {
|
|||||||
/// 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
|
|||||||
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,7 +501,115 @@ 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,
|
||||||
|
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(tester.getCenter(find.byType(RefreshProgressIndicator)).dy, lessThan(300.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
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(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;
|
refreshCalled = false;
|
||||||
final ScrollController scrollController = ScrollController();
|
final ScrollController scrollController = ScrollController();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -532,10 +640,10 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
|
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
|
||||||
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, lessThan(300.0));
|
expect(find.byType(RefreshProgressIndicator), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Bottom RefreshIndicator showed when dragging from non-zero scroll position', (WidgetTester tester) async {
|
testWidgets('Bottom RefreshIndicator(onEdge 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(
|
||||||
@ -567,6 +675,6 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
|
await tester.pump(const Duration(seconds: 1)); // finish the scroll animation
|
||||||
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(find.byType(RefreshProgressIndicator), findsNothing);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user