mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 11:08:03 +08:00
@@ -2,18 +2,13 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'color_scheme.dart';
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:async' show Completer;
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/scroll_behavior.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/material.dart' hide RefreshIndicator;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
double displacement = Pref.refreshDisplacement;
|
||||
|
||||
@@ -33,21 +28,13 @@ const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
|
||||
// has completed.
|
||||
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
|
||||
|
||||
/// The signature for a function that's called when the user has dragged a
|
||||
/// [RefreshIndicator] far enough to demonstrate that they want the app to
|
||||
/// refresh. The returned [Future] must complete when the refresh operation is
|
||||
/// finished.
|
||||
///
|
||||
/// Used by [RefreshIndicator.onRefresh].
|
||||
typedef RefreshCallback = Future<void> Function();
|
||||
|
||||
/// 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,
|
||||
// armed,
|
||||
|
||||
/// Animating to the indicator's final "displacement".
|
||||
snap,
|
||||
@@ -62,19 +49,6 @@ enum RefreshIndicatorStatus {
|
||||
canceled,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
enum _IndicatorType { material, adaptive, noSpinner }
|
||||
|
||||
/// A widget that supports the Material "swipe to refresh" idiom.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
|
||||
@@ -155,74 +129,11 @@ class RefreshIndicator extends StatefulWidget {
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
this.isClampingScrollPhysics = false,
|
||||
required this.child,
|
||||
}) : _indicatorType = _IndicatorType.material,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// Creates an adaptive [RefreshIndicator] based on whether the target
|
||||
/// platform is iOS or macOS, following Material design's
|
||||
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
|
||||
///
|
||||
/// When the descendant overscrolls, a different spinning progress indicator
|
||||
/// is shown depending on platform. On iOS and macOS,
|
||||
/// [CupertinoActivityIndicator] is shown, but on all other platforms,
|
||||
/// [CircularProgressIndicator] appears.
|
||||
///
|
||||
/// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
|
||||
/// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
|
||||
///
|
||||
/// The target platform is based on the current [Theme]: [ThemeData.platform].
|
||||
///
|
||||
/// Notably the scrollable widget itself will have slightly different behavior
|
||||
/// from [CupertinoSliverRefreshControl], due to a difference in structure.
|
||||
const RefreshIndicator.adaptive({
|
||||
super.key,
|
||||
this.displacement = 40.0,
|
||||
this.edgeOffset = 0.0,
|
||||
required this.onRefresh,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
required this.child,
|
||||
}) : _indicatorType = _IndicatorType.adaptive,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// 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.onRefresh,
|
||||
this.onStatusChange,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
required this.child,
|
||||
}) : _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,
|
||||
assert(elevation >= 0.0);
|
||||
}) : assert(elevation >= 0.0);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
@@ -262,10 +173,6 @@ 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;
|
||||
@@ -281,42 +188,18 @@ class RefreshIndicator extends StatefulWidget {
|
||||
/// else for more complicated layouts.
|
||||
final ScrollNotificationPredicate notificationPredicate;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel}
|
||||
///
|
||||
/// This will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]
|
||||
/// if it is null.
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue}
|
||||
final String? semanticsValue;
|
||||
|
||||
/// Defines [strokeWidth] for `RefreshIndicator`.
|
||||
///
|
||||
/// By default, the value of [strokeWidth] is 2.0 pixels.
|
||||
final double strokeWidth;
|
||||
|
||||
final _IndicatorType _indicatorType;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// Defines the elevation of the underlying [RefreshIndicator].
|
||||
///
|
||||
/// Defaults to 2.0.
|
||||
final double elevation;
|
||||
|
||||
final bool isClampingScrollPhysics;
|
||||
|
||||
@override
|
||||
RefreshIndicatorState createState() => RefreshIndicatorState();
|
||||
}
|
||||
@@ -324,17 +207,14 @@ 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> {
|
||||
with SingleTickerProviderStateMixin<RefreshIndicator> {
|
||||
late AnimationController _positionController;
|
||||
late AnimationController _scaleController;
|
||||
late Animation<double> _positionFactor;
|
||||
late Animation<double> _scaleFactor;
|
||||
late Animation<double> _value;
|
||||
late Animation<Color?> _valueColor;
|
||||
|
||||
RefreshIndicatorStatus? _status;
|
||||
late Future<void> _pendingRefreshFuture;
|
||||
bool? _isIndicatorAtTop;
|
||||
double? _dragOffset;
|
||||
late Color _effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
@@ -349,11 +229,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
end: _kDragSizeFactorLimit,
|
||||
);
|
||||
|
||||
static final Animatable<double> _oneToZeroTween = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
);
|
||||
|
||||
@protected
|
||||
@override
|
||||
void initState() {
|
||||
@@ -363,9 +238,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
|
||||
// The "value" of the circular progress indicator during a drag.
|
||||
_value = _positionController.drive(_threeQuarterTween);
|
||||
|
||||
_scaleController = AnimationController(vsync: this);
|
||||
_scaleFactor = _scaleController.drive(_oneToZeroTween);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -388,7 +260,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
@override
|
||||
void dispose() {
|
||||
_positionController.dispose();
|
||||
_scaleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -417,77 +288,52 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
// 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)) &&
|
||||
return (notification is ScrollStartNotification &&
|
||||
notification.dragDetails != null) &&
|
||||
notification.metrics.extentBefore == 0.0 &&
|
||||
_status == null &&
|
||||
_start(notification.metrics.axisDirection);
|
||||
_start();
|
||||
}
|
||||
|
||||
double? _viewportDimension;
|
||||
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (!widget.notificationPredicate(notification)) {
|
||||
return false;
|
||||
}
|
||||
if (_shouldStart(notification)) {
|
||||
_viewportDimension = notification.metrics.viewportDimension;
|
||||
setState(() {
|
||||
_status = RefreshIndicatorStatus.drag;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
final bool? indicatorAtTopNow =
|
||||
switch (notification.metrics.axisDirection) {
|
||||
AxisDirection.down || AxisDirection.up => true,
|
||||
AxisDirection.left || AxisDirection.right => null,
|
||||
};
|
||||
if (indicatorAtTopNow != _isIndicatorAtTop) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
}
|
||||
} else if (notification is ScrollUpdateNotification) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.scrollDelta!;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.scrollDelta!;
|
||||
}
|
||||
if (notification is ScrollUpdateNotification) {
|
||||
if (_status == RefreshIndicatorStatus.drag) {
|
||||
_dragOffset = _dragOffset! - notification.scrollDelta!;
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
if (_status == RefreshIndicatorStatus.armed &&
|
||||
notification.dragDetails == null) {
|
||||
if (_status == RefreshIndicatorStatus.drag &&
|
||||
notification.dragDetails == null &&
|
||||
_valueColor.value!.a == _effectiveValueColor.a) {
|
||||
// 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 (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.overscroll;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.overscroll;
|
||||
}
|
||||
if (_status == RefreshIndicatorStatus.drag) {
|
||||
_dragOffset = _dragOffset! - notification.overscroll;
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
switch (_status) {
|
||||
case RefreshIndicatorStatus.armed:
|
||||
if (_positionController.value < 1.0) {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
} else {
|
||||
_show();
|
||||
}
|
||||
case RefreshIndicatorStatus.drag:
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
if (_valueColor.value!.a == _effectiveValueColor.a) {
|
||||
_show();
|
||||
} else {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
}
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
case RefreshIndicatorStatus.done:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
@@ -513,46 +359,25 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _start(AxisDirection direction) {
|
||||
bool _start() {
|
||||
assert(_status == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
assert(_dragOffset == null);
|
||||
switch (direction) {
|
||||
case AxisDirection.down:
|
||||
case AxisDirection.up:
|
||||
_isIndicatorAtTop = true;
|
||||
case AxisDirection.left:
|
||||
case AxisDirection.right:
|
||||
_isIndicatorAtTop = null;
|
||||
// we do not support horizontal scroll views.
|
||||
return false;
|
||||
}
|
||||
_dragOffset = 0.0;
|
||||
_scaleController.value = 0.0;
|
||||
_positionController.value = 0.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void _checkDragOffset(double containerExtent) {
|
||||
assert(
|
||||
_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed,
|
||||
_status == RefreshIndicatorStatus.drag,
|
||||
);
|
||||
double newValue =
|
||||
_dragOffset! / (containerExtent * kDragContainerExtentPercentage);
|
||||
if (_status == RefreshIndicatorStatus.armed) {
|
||||
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
|
||||
}
|
||||
_positionController.value = clampDouble(
|
||||
newValue,
|
||||
0.0,
|
||||
1.0,
|
||||
); // This triggers various rebuilds.
|
||||
if (_status == RefreshIndicatorStatus.drag &&
|
||||
_valueColor.value!.a == _effectiveValueColor.a) {
|
||||
_status = RefreshIndicatorStatus.armed;
|
||||
widget.onStatusChange?.call(_status);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop showing the refresh indicator.
|
||||
@@ -567,20 +392,15 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
);
|
||||
setState(() {
|
||||
_status = newMode;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
switch (_status!) {
|
||||
case RefreshIndicatorStatus.done:
|
||||
await _scaleController.animateTo(
|
||||
1.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
break;
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
await _positionController.animateTo(
|
||||
0.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
case RefreshIndicatorStatus.armed:
|
||||
case RefreshIndicatorStatus.drag:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
case RefreshIndicatorStatus.snap:
|
||||
@@ -588,7 +408,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
}
|
||||
if (mounted && _status == newMode) {
|
||||
_dragOffset = null;
|
||||
_isIndicatorAtTop = null;
|
||||
setState(() {
|
||||
_status = null;
|
||||
});
|
||||
@@ -601,7 +420,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
final Completer<void> completer = Completer<void>();
|
||||
_pendingRefreshFuture = completer.future;
|
||||
_status = RefreshIndicatorStatus.snap;
|
||||
widget.onStatusChange?.call(_status);
|
||||
_positionController
|
||||
.animateTo(
|
||||
1.0 / _kDragSizeFactorLimit,
|
||||
@@ -640,11 +458,11 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
/// 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}) {
|
||||
Future<void> show() {
|
||||
if (_status != RefreshIndicatorStatus.refresh &&
|
||||
_status != RefreshIndicatorStatus.snap) {
|
||||
if (_status == null) {
|
||||
_start(atTop ? AxisDirection.down : AxisDirection.up);
|
||||
_start();
|
||||
}
|
||||
_show();
|
||||
}
|
||||
@@ -655,7 +473,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final Widget child = NotificationListener<ScrollNotification>(
|
||||
Widget child = NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: NotificationListener<OverscrollIndicatorNotification>(
|
||||
onNotification: _handleIndicatorNotification,
|
||||
@@ -665,10 +483,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
assert(() {
|
||||
if (_status == null) {
|
||||
assert(_dragOffset == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
} else {
|
||||
assert(_dragOffset != null);
|
||||
assert(_isIndicatorAtTop != null);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
@@ -677,75 +493,30 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
_status == RefreshIndicatorStatus.refresh ||
|
||||
_status == RefreshIndicatorStatus.done;
|
||||
|
||||
return Stack(
|
||||
child = Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
child,
|
||||
if (_status != null)
|
||||
Positioned(
|
||||
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
top: widget.edgeOffset,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
child: SizeTransition(
|
||||
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
|
||||
axisAlignment: 1.0,
|
||||
sizeFactor: _positionFactor, // This is what brings it down.
|
||||
child: Padding(
|
||||
padding: _isIndicatorAtTop!
|
||||
? EdgeInsets.only(top: widget.displacement)
|
||||
: EdgeInsets.only(bottom: widget.displacement),
|
||||
padding: EdgeInsets.only(top: widget.displacement),
|
||||
child: Align(
|
||||
alignment: _isIndicatorAtTop!
|
||||
? 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,
|
||||
semanticsValue: widget.semanticsValue,
|
||||
value: showIndeterminateIndicator
|
||||
? null
|
||||
: _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
elevation: widget.elevation,
|
||||
);
|
||||
|
||||
final Widget cupertinoIndicator =
|
||||
CupertinoActivityIndicator(
|
||||
color: widget.color,
|
||||
);
|
||||
|
||||
switch (widget._indicatorType) {
|
||||
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.noSpinner:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
},
|
||||
alignment: Alignment.topCenter,
|
||||
child: AnimatedBuilder(
|
||||
animation: _positionController,
|
||||
builder: (context, child) => RefreshProgressIndicator(
|
||||
value: showIndeterminateIndicator ? null : _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
elevation: widget.elevation,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -754,16 +525,81 @@ class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
),
|
||||
],
|
||||
);
|
||||
if (!widget.isClampingScrollPhysics &&
|
||||
(Platform.isIOS || Platform.isMacOS)) {
|
||||
return child;
|
||||
}
|
||||
return ScrollConfiguration(
|
||||
behavior: RefreshScrollBehavior(
|
||||
desktopDragDevices,
|
||||
scrollPhysics: RefreshScrollPhysics(onDrag: _onDrag),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
bool _onDrag(double offset) {
|
||||
if (_positionController.value > 0.0 &&
|
||||
_status == RefreshIndicatorStatus.drag &&
|
||||
_viewportDimension != null) {
|
||||
_dragOffset = _dragOffset! + offset;
|
||||
_checkDragOffset(_viewportDimension!);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Widget refreshIndicator({
|
||||
required RefreshCallback onRefresh,
|
||||
required Widget child,
|
||||
bool isClampingScrollPhysics = false,
|
||||
}) {
|
||||
return RefreshIndicator(
|
||||
displacement: displacement,
|
||||
onRefresh: onRefresh,
|
||||
isClampingScrollPhysics: isClampingScrollPhysics,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
class RefreshScrollBehavior extends CustomScrollBehavior {
|
||||
const RefreshScrollBehavior(
|
||||
super.dragDevices, {
|
||||
required this.scrollPhysics,
|
||||
});
|
||||
|
||||
final RefreshScrollPhysics scrollPhysics;
|
||||
|
||||
@override
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) {
|
||||
return scrollPhysics;
|
||||
}
|
||||
}
|
||||
|
||||
typedef OnDrag = bool Function(double offset);
|
||||
|
||||
class RefreshScrollPhysics extends ClampingScrollPhysics {
|
||||
const RefreshScrollPhysics({
|
||||
super.parent,
|
||||
required this.onDrag,
|
||||
});
|
||||
|
||||
final OnDrag onDrag;
|
||||
|
||||
@override
|
||||
RefreshScrollPhysics applyTo(ScrollPhysics? ancestor) {
|
||||
return RefreshScrollPhysics(
|
||||
parent: buildParent(ancestor),
|
||||
onDrag: onDrag,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
|
||||
if (offset < 0.0 && onDrag(offset)) {
|
||||
return 0.0;
|
||||
}
|
||||
return parent?.applyPhysicsToUserOffset(position, offset) ?? offset;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user