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.
|
||||
}
|
||||
|
||||
/// 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,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;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
@ -532,10 +640,10 @@ void main() {
|
||||
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));
|
||||
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;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
@ -567,6 +675,6 @@ void main() {
|
||||
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));
|
||||
expect(find.byType(RefreshProgressIndicator), findsNothing);
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user