forked from firka/flutter
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:
@@ -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',
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user