Added new constructor RefreshIndicator.noSpinner() (#152075)

This PR adds a new constructor to the RefreshIndicator's class, which is `noSpinner`.
The purpose of this new constructor is to create a RefreshIndicator that doesn't show a spinner when the user arms it by pulling.

The work is based on a partial that is here: https://github.com/flutter/flutter/pull/133507

I addressed the following issues reported in the PR above:
- in the example for `noSpinner`, arming the RefreshIndicator now shows a CircularProgressIndicator, instead of just printing text to the console;
- added a test for the new example;
- added a doc comment on the new constructor;

Fixes https://github.com/flutter/flutter/issues/132775
This commit is contained in:
Pavlo Kochylo
2024-08-23 22:37:24 +02:00
committed by GitHub
parent 5e194383af
commit a4b0d973fb
4 changed files with 360 additions and 91 deletions

View File

@@ -0,0 +1,95 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Flutter code sample for [RefreshIndicator.noSpinner].
void main() => runApp(const RefreshIndicatorExampleApp());
class RefreshIndicatorExampleApp extends StatelessWidget {
const RefreshIndicatorExampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: RefreshIndicatorExample(),
);
}
}
class RefreshIndicatorExample extends StatefulWidget {
const RefreshIndicatorExample({super.key});
@override
State<RefreshIndicatorExample> createState() => _RefreshIndicatorExampleState();
}
class _RefreshIndicatorExampleState extends State<RefreshIndicatorExample> {
bool _isRefreshing = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('RefreshIndicator.noSpinner Sample'),
),
body: Stack(
children: <Widget>[
RefreshIndicator.noSpinner(
// Callback function used by the app to listen to the
// status of the RefreshIndicator pull-down action.
onStatusChange: (RefreshIndicatorStatus? status) {
if (status == RefreshIndicatorStatus.done) {
setState(() {
_isRefreshing = false;
});
}
},
// Callback that gets called whenever the user pulls down to refresh.
onRefresh: () async {
// This can be also done in onStatusChange when the status is RefreshIndicatorStatus.refresh.
setState(() {
_isRefreshing = true;
});
// Replace this delay with the code to be executed during refresh
// and return asynchronous code.
return Future<void>.delayed(const Duration(seconds: 3));
},
child: CustomScrollView(
slivers: <Widget>[
SliverList.builder(
itemCount: 20,
itemBuilder: (BuildContext context, int index) {
return ListTile(
tileColor: Colors.green[100],
title: const Text('Pull down here'),
subtitle: const Text('A custom refresh indicator will be shown'),
);
}
)
],
),
),
// Shows an overlay with a CircularProgressIndicator when refreshing.
if (_isRefreshing)
ColoredBox(
color: Colors.black45,
child: Align(
child: CircularProgressIndicator(
color: Colors.purple[500],
strokeWidth: 10,
semanticsLabel: 'Circular progress indicator',
),
),
),
]
),
);
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/material/refresh_indicator/refresh_indicator.2.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Pulling from scroll view triggers a refresh indicator which shows a CircularProgressIndicator', (WidgetTester tester) async {
await tester.pumpWidget(
const example.RefreshIndicatorExampleApp(),
);
// Pull the first item.
await tester.fling(find.text('Pull down here').first, const Offset(0.0, 300.0), 1000.0);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(find.byType(RefreshProgressIndicator), findsNothing);
expect(find.bySemanticsLabel('Circular progress indicator'), findsOneWidget);
await tester.pumpAndSettle(); // Advance pending time.
});
}

View File

@@ -40,15 +40,25 @@ const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
/// Used by [RefreshIndicator.onRefresh].
typedef RefreshCallback = Future<void> Function();
// The state machine moves through these modes only when the scrollable
// identified by scrollableKey has been scrolled to its min or max limit.
enum _RefreshIndicatorMode {
drag, // Pointer is down.
armed, // Dragged far enough that an up event will run the onRefresh callback.
snap, // Animating to the indicator's final "displacement".
refresh, // Running the refresh callback.
done, // Animating the indicator's fade-out after refreshing.
canceled, // Animating the indicator's fade-out after not arming.
/// Indicates current status of Material `RefreshIndicator`.
enum RefreshIndicatorStatus {
/// Pointer is down.
drag,
/// Dragged far enough that an up event will run the onRefresh callback.
armed,
/// Animating to the indicator's final "displacement".
snap,
/// Running the refresh callback.
refresh,
/// Animating the indicator's fade-out after refreshing.
done,
/// Animating the indicator's fade-out after not arming.
canceled,
}
/// Used to configure how [RefreshIndicator] can be triggered.
@@ -62,7 +72,7 @@ enum RefreshIndicatorTriggerMode {
onEdge,
}
enum _IndicatorType { material, adaptive }
enum _IndicatorType { material, adaptive, noSpinner }
/// A widget that supports the Material "swipe to refresh" idiom.
///
@@ -90,6 +100,12 @@ enum _IndicatorType { material, adaptive }
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how to use [RefreshIndicator] without the spinner.
///
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart **
/// {@end-tool}
///
/// ## Troubleshooting
///
/// ### Refresh indicator does not show up
@@ -143,7 +159,8 @@ class RefreshIndicator extends StatefulWidget {
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.material;
}) : _indicatorType = _IndicatorType.material,
onStatusChange = null;
/// Creates an adaptive [RefreshIndicator] based on whether the target
/// platform is iOS or macOS, following Material design's
@@ -174,7 +191,31 @@ class RefreshIndicator extends StatefulWidget {
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.adaptive;
}) : _indicatorType = _IndicatorType.adaptive,
onStatusChange = null;
/// Creates a [RefreshIndicator] with no spinner and calls `onRefresh` when
/// successfully armed by a drag event.
///
/// Events can be optionally listened by using the `onStatusChange` callback.
const RefreshIndicator.noSpinner({
super.key,
required this.child,
required this.onRefresh,
this.onStatusChange,
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
}) : _indicatorType = _IndicatorType.noSpinner,
// The following parameters aren't used because [_IndicatorType.noSpinner] is being used,
// which involves showing no spinner, hence the following parameters are useless since
// their only use is to change the spinner's appearance.
displacement = 0.0,
edgeOffset = 0.0,
color = null,
backgroundColor = null,
strokeWidth = 0.0;
/// The widget below this widget in the tree.
///
@@ -214,6 +255,10 @@ class RefreshIndicator extends StatefulWidget {
/// [Future] must complete when the refresh operation is finished.
final RefreshCallback onRefresh;
/// Called to get the current status of the [RefreshIndicator] to update the UI as needed.
/// This is an optional parameter, used to fine tune app cases.
final ValueChanged<RefreshIndicatorStatus?>? onStatusChange;
/// The progress indicator's foreground color. The current theme's
/// [ColorScheme.primary] by default.
final Color? color;
@@ -266,7 +311,8 @@ class RefreshIndicator extends StatefulWidget {
/// Contains the state for a [RefreshIndicator]. This class can be used to
/// programmatically show the refresh indicator, see the [show] method.
class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderStateMixin<RefreshIndicator> {
class RefreshIndicatorState extends State<RefreshIndicator>
with TickerProviderStateMixin<RefreshIndicator> {
late AnimationController _positionController;
late AnimationController _scaleController;
late Animation<double> _positionFactor;
@@ -274,22 +320,35 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
late Animation<double> _value;
late Animation<Color?> _valueColor;
_RefreshIndicatorMode? _mode;
RefreshIndicatorStatus? _status;
late Future<void> _pendingRefreshFuture;
bool? _isIndicatorAtTop;
double? _dragOffset;
late Color _effectiveValueColor = widget.color ?? Theme.of(context).colorScheme.primary;
static final Animatable<double> _threeQuarterTween = Tween<double>(begin: 0.0, end: 0.75);
static final Animatable<double> _kDragSizeFactorLimitTween = Tween<double>(begin: 0.0, end: _kDragSizeFactorLimit);
static final Animatable<double> _oneToZeroTween = Tween<double>(begin: 1.0, end: 0.0);
static final Animatable<double> _threeQuarterTween = Tween<double>(
begin: 0.0,
end: 0.75,
);
static final Animatable<double> _kDragSizeFactorLimitTween = Tween<double>(
begin: 0.0,
end: _kDragSizeFactorLimit,
);
static final Animatable<double> _oneToZeroTween = Tween<double>(
begin: 1.0,
end: 0.0,
);
@override
void initState() {
super.initState();
_positionController = AnimationController(vsync: this);
_positionFactor = _positionController.drive(_kDragSizeFactorLimitTween);
_value = _positionController.drive(_threeQuarterTween); // The "value" of the circular progress indicator during a drag.
// The "value" of the circular progress indicator during a drag.
_value = _positionController.drive(_threeQuarterTween);
_scaleController = AnimationController(vsync: this);
_scaleFactor = _scaleController.drive(_oneToZeroTween);
@@ -342,12 +401,26 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
// If the notification.dragDetails is null, this scroll is not triggered by
// user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll.
// In this case, we don't want to trigger the refresh indicator.
return ((notification is ScrollStartNotification && notification.dragDetails != null)
|| (notification is ScrollUpdateNotification && notification.dragDetails != null && widget.triggerMode == RefreshIndicatorTriggerMode.anywhere))
&& (( notification.metrics.axisDirection == AxisDirection.up && notification.metrics.extentAfter == 0.0)
|| (notification.metrics.axisDirection == AxisDirection.down && notification.metrics.extentBefore == 0.0))
&& _mode == null
&& _start(notification.metrics.axisDirection);
return (
(
notification is ScrollStartNotification
&& notification.dragDetails != null
) || (
notification is ScrollUpdateNotification
&& notification.dragDetails != null
&& widget.triggerMode == RefreshIndicatorTriggerMode.anywhere
)
)
&& (
(
notification.metrics.axisDirection == AxisDirection.up
&& notification.metrics.extentAfter == 0.0
) || (
notification.metrics.axisDirection == AxisDirection.down
&& notification.metrics.extentBefore == 0.0
)
)
&& _status == null && _start(notification.metrics.axisDirection);
}
bool _handleScrollNotification(ScrollNotification notification) {
@@ -356,7 +429,8 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
}
if (_shouldStart(notification)) {
setState(() {
_mode = _RefreshIndicatorMode.drag;
_status = RefreshIndicatorStatus.drag;
widget.onStatusChange?.call(_status);
});
return false;
}
@@ -365,11 +439,13 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
AxisDirection.left || AxisDirection.right => null,
};
if (indicatorAtTopNow != _isIndicatorAtTop) {
if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
_dismiss(_RefreshIndicatorMode.canceled);
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
_dismiss(RefreshIndicatorStatus.canceled);
}
} else if (notification is ScrollUpdateNotification) {
if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.scrollDelta!;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
@@ -377,14 +453,16 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
}
_checkDragOffset(notification.metrics.viewportDimension);
}
if (_mode == _RefreshIndicatorMode.armed && notification.dragDetails == null) {
if (_status == RefreshIndicatorStatus.armed &&
notification.dragDetails == null) {
// On iOS start the refresh when the Scrollable bounces back from the
// overscroll (ScrollNotification indicating this don't have dragDetails
// because the scroll activity is not directly triggered by a drag).
_show();
}
} else if (notification is OverscrollNotification) {
if (_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed) {
if (_status == RefreshIndicatorStatus.drag ||
_status == RefreshIndicatorStatus.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.overscroll;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
@@ -393,19 +471,19 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
_checkDragOffset(notification.metrics.viewportDimension);
}
} else if (notification is ScrollEndNotification) {
switch (_mode) {
case _RefreshIndicatorMode.armed:
switch (_status) {
case RefreshIndicatorStatus.armed:
if (_positionController.value < 1.0) {
_dismiss(_RefreshIndicatorMode.canceled);
_dismiss(RefreshIndicatorStatus.canceled);
} else {
_show();
}
case _RefreshIndicatorMode.drag:
_dismiss(_RefreshIndicatorMode.canceled);
case _RefreshIndicatorMode.canceled:
case _RefreshIndicatorMode.done:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
case RefreshIndicatorStatus.drag:
_dismiss(RefreshIndicatorStatus.canceled);
case RefreshIndicatorStatus.canceled:
case RefreshIndicatorStatus.done:
case RefreshIndicatorStatus.refresh:
case RefreshIndicatorStatus.snap:
case null:
// do nothing
break;
@@ -418,7 +496,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
if (notification.depth != 0 || !notification.leading) {
return false;
}
if (_mode == _RefreshIndicatorMode.drag) {
if (_status == RefreshIndicatorStatus.drag) {
notification.disallowIndicator();
return true;
}
@@ -426,7 +504,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
}
bool _start(AxisDirection direction) {
assert(_mode == null);
assert(_status == null);
assert(_isIndicatorAtTop == null);
assert(_dragOffset == null);
switch (direction) {
@@ -446,67 +524,74 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
}
void _checkDragOffset(double containerExtent) {
assert(_mode == _RefreshIndicatorMode.drag || _mode == _RefreshIndicatorMode.armed);
assert(
_status == RefreshIndicatorStatus.drag
|| _status == RefreshIndicatorStatus.armed
);
double newValue = _dragOffset! / (containerExtent * _kDragContainerExtentPercentage);
if (_mode == _RefreshIndicatorMode.armed) {
if (_status == RefreshIndicatorStatus.armed) {
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
}
_positionController.value = clampDouble(newValue, 0.0, 1.0); // this triggers various rebuilds
if (_mode == _RefreshIndicatorMode.drag && _valueColor.value!.alpha == _effectiveValueColor.alpha) {
_mode = _RefreshIndicatorMode.armed;
_positionController.value = clampDouble(newValue, 0.0, 1.0); // This triggers various rebuilds.
if (_status == RefreshIndicatorStatus.drag && _valueColor.value!.alpha == _effectiveValueColor.alpha) {
_status = RefreshIndicatorStatus.armed;
widget.onStatusChange?.call(_status);
}
}
// Stop showing the refresh indicator.
Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
Future<void> _dismiss(RefreshIndicatorStatus newMode) async {
await Future<void>.value();
// This can only be called from _show() when refreshing and
// _handleScrollNotification in response to a ScrollEndNotification or
// direction change.
assert(newMode == _RefreshIndicatorMode.canceled || newMode == _RefreshIndicatorMode.done);
assert(newMode == RefreshIndicatorStatus.canceled ||
newMode == RefreshIndicatorStatus.done);
setState(() {
_mode = newMode;
_status = newMode;
widget.onStatusChange?.call(_status);
});
switch (_mode!) {
case _RefreshIndicatorMode.done:
switch (_status!) {
case RefreshIndicatorStatus.done:
await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
case _RefreshIndicatorMode.canceled:
case RefreshIndicatorStatus.canceled:
await _positionController.animateTo(0.0, duration: _kIndicatorScaleDuration);
case _RefreshIndicatorMode.armed:
case _RefreshIndicatorMode.drag:
case _RefreshIndicatorMode.refresh:
case _RefreshIndicatorMode.snap:
case RefreshIndicatorStatus.armed:
case RefreshIndicatorStatus.drag:
case RefreshIndicatorStatus.refresh:
case RefreshIndicatorStatus.snap:
assert(false);
}
if (mounted && _mode == newMode) {
if (mounted && _status == newMode) {
_dragOffset = null;
_isIndicatorAtTop = null;
setState(() {
_mode = null;
_status = null;
});
}
}
void _show() {
assert(_mode != _RefreshIndicatorMode.refresh);
assert(_mode != _RefreshIndicatorMode.snap);
assert(_status != RefreshIndicatorStatus.refresh);
assert(_status != RefreshIndicatorStatus.snap);
final Completer<void> completer = Completer<void>();
_pendingRefreshFuture = completer.future;
_mode = _RefreshIndicatorMode.snap;
_status = RefreshIndicatorStatus.snap;
widget.onStatusChange?.call(_status);
_positionController
.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration)
.then<void>((void value) {
if (mounted && _mode == _RefreshIndicatorMode.snap) {
if (mounted && _status == RefreshIndicatorStatus.snap) {
setState(() {
// Show the indeterminate progress indicator.
_mode = _RefreshIndicatorMode.refresh;
_status = RefreshIndicatorStatus.refresh;
});
final Future<void> refreshResult = widget.onRefresh();
refreshResult.whenComplete(() {
if (mounted && _mode == _RefreshIndicatorMode.refresh) {
if (mounted && _status == RefreshIndicatorStatus.refresh) {
completer.complete();
_dismiss(_RefreshIndicatorMode.done);
_dismiss(RefreshIndicatorStatus.done);
}
});
}
@@ -529,10 +614,10 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
/// When initiated in this manner, the refresh indicator is independent of any
/// actual scroll view. It defaults to showing the indicator at the top. To
/// show it at the bottom, set `atTop` to false.
Future<void> show({ bool atTop = true }) {
if (_mode != _RefreshIndicatorMode.refresh &&
_mode != _RefreshIndicatorMode.snap) {
if (_mode == null) {
Future<void> show({bool atTop = true}) {
if (_status != RefreshIndicatorStatus.refresh &&
_status != RefreshIndicatorStatus.snap) {
if (_status == null) {
_start(atTop ? AxisDirection.down : AxisDirection.up);
}
_show();
@@ -551,7 +636,7 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
),
);
assert(() {
if (_mode == null) {
if (_status == null) {
assert(_dragOffset == null);
assert(_isIndicatorAtTop == null);
} else {
@@ -561,34 +646,36 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
return true;
}());
final bool showIndeterminateIndicator =
_mode == _RefreshIndicatorMode.refresh || _mode == _RefreshIndicatorMode.done;
final bool showIndeterminateIndicator = _status == RefreshIndicatorStatus.refresh
|| _status == RefreshIndicatorStatus.done;
return Stack(
children: <Widget>[
child,
if (_mode != null) Positioned(
if (_status != null) Positioned(
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
left: 0.0,
right: 0.0,
child: SizeTransition(
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
sizeFactor: _positionFactor, // this is what brings it down
sizeFactor: _positionFactor, // This is what brings it down.
child: Container(
padding: _isIndicatorAtTop!
? EdgeInsets.only(top: widget.displacement)
: EdgeInsets.only(bottom: widget.displacement),
? EdgeInsets.only(top: widget.displacement)
: EdgeInsets.only(bottom: widget.displacement),
alignment: _isIndicatorAtTop!
? Alignment.topCenter
: Alignment.bottomCenter,
? Alignment.topCenter
: Alignment.bottomCenter,
child: ScaleTransition(
scale: _scaleFactor,
child: AnimatedBuilder(
animation: _positionController,
builder: (BuildContext context, Widget? child) {
final Widget materialIndicator = RefreshProgressIndicator(
semanticsLabel: widget.semanticsLabel ?? MaterialLocalizations.of(context).refreshIndicatorSemanticLabel,
semanticsLabel: widget.semanticsLabel ??
MaterialLocalizations.of(context)
.refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator ? null : _value.value,
valueColor: _valueColor,
@@ -604,19 +691,22 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
case _IndicatorType.material:
return materialIndicator;
case _IndicatorType.adaptive: {
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return materialIndicator;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return cupertinoIndicator;
case _IndicatorType.adaptive:
{
final ThemeData theme = Theme.of(context);
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return materialIndicator;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return cupertinoIndicator;
}
}
}
case _IndicatorType.noSpinner:
return Container();
}
},
),

View File

@@ -1140,6 +1140,64 @@ void main() {
expect(stretchAccepted, false);
});
group('RefreshIndicator.noSpinner', () {
testWidgets('onStatusChange and onRefresh Trigger', (WidgetTester tester) async {
refreshCalled = false;
bool modeSnap = false;
bool modeDrag = false;
bool modeArmed = false;
bool modeDone = false;
await tester.pumpWidget(MaterialApp(
home: RefreshIndicator.noSpinner(
onStatusChange: (RefreshIndicatorStatus? mode) {
if (mode == RefreshIndicatorStatus.armed) {
modeArmed = true;
}
if (mode == RefreshIndicatorStatus.drag) {
modeDrag = true;
}
if (mode == RefreshIndicatorStatus.snap) {
modeSnap = true;
}
if (mode == RefreshIndicatorStatus.done) {
modeDone = true;
}
},
onRefresh: refresh,
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children:
<String>['A', 'B', 'C', 'D', 'E', 'F'].map<Widget>((String item) {
return SizedBox(
height: 200.0,
child: Text(item),
);
}).toList(),
),
),
));
await tester.fling(find.text('A'), const Offset(0.0, 300.0), 1000.0);
await tester.pump();
// 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 hide animation.
await tester.pump(const Duration(seconds: 1));
expect(refreshCalled, true);
expect(modeSnap, true);
expect(modeDrag, true);
expect(modeArmed, true);
expect(modeDone, true);
});
});
testWidgets('RefreshIndicator manipulates value color opacity correctly', (WidgetTester tester) async {
final List<Color> colors = <Color>[
Colors.black,