opt refresh

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-02-22 13:48:46 +08:00
parent 7e81fae2bc
commit 7563a52bed
5 changed files with 118 additions and 279 deletions

View File

@@ -2,18 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// ignore_for_file: uri_does_not_exist_in_doc_import import 'dart:async' show Completer;
import 'dart:io' show Platform;
/// @docImport 'color_scheme.dart';
library;
import 'dart:async';
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/scroll_behavior.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart' hide RefreshIndicator; import 'package:flutter/material.dart';
double displacement = Pref.refreshDisplacement; double displacement = Pref.refreshDisplacement;
@@ -33,21 +28,13 @@ const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
// has completed. // has completed.
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200); 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`. /// Indicates current status of Material `RefreshIndicator`.
enum RefreshIndicatorStatus { enum RefreshIndicatorStatus {
/// Pointer is down. /// Pointer is down.
drag, drag,
/// Dragged far enough that an up event will run the onRefresh callback. /// Dragged far enough that an up event will run the onRefresh callback.
armed, // armed,
/// Animating to the indicator's final "displacement". /// Animating to the indicator's final "displacement".
snap, snap,
@@ -62,19 +49,6 @@ enum RefreshIndicatorStatus {
canceled, 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. /// A widget that supports the Material "swipe to refresh" idiom.
/// ///
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM} /// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
@@ -155,74 +129,11 @@ class RefreshIndicator extends StatefulWidget {
this.color, this.color,
this.backgroundColor, this.backgroundColor,
this.notificationPredicate = defaultScrollNotificationPredicate, this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel,
this.semanticsValue,
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth, this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
this.elevation = 2.0, this.elevation = 2.0,
this.isClampingScrollPhysics = false,
required this.child, required this.child,
}) : _indicatorType = _IndicatorType.material, }) : assert(elevation >= 0.0);
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);
/// The widget below this widget in the tree. /// 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. /// [Future] must complete when the refresh operation is finished.
final RefreshCallback onRefresh; 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 /// The progress indicator's foreground color. The current theme's
/// [ColorScheme.primary] by default. /// [ColorScheme.primary] by default.
final Color? color; final Color? color;
@@ -281,42 +188,18 @@ class RefreshIndicator extends StatefulWidget {
/// else for more complicated layouts. /// else for more complicated layouts.
final ScrollNotificationPredicate notificationPredicate; 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`. /// Defines [strokeWidth] for `RefreshIndicator`.
/// ///
/// 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;
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]. /// Defines the elevation of the underlying [RefreshIndicator].
/// ///
/// Defaults to 2.0. /// Defaults to 2.0.
final double elevation; final double elevation;
final bool isClampingScrollPhysics;
@override @override
RefreshIndicatorState createState() => RefreshIndicatorState(); RefreshIndicatorState createState() => RefreshIndicatorState();
} }
@@ -324,17 +207,14 @@ class RefreshIndicator extends StatefulWidget {
/// Contains the state for a [RefreshIndicator]. This class can be used to /// Contains the state for a [RefreshIndicator]. This class can be used to
/// programmatically show the refresh indicator, see the [show] method. /// programmatically show the refresh indicator, see the [show] method.
class RefreshIndicatorState extends State<RefreshIndicator> class RefreshIndicatorState extends State<RefreshIndicator>
with TickerProviderStateMixin<RefreshIndicator> { with SingleTickerProviderStateMixin<RefreshIndicator> {
late AnimationController _positionController; late AnimationController _positionController;
late AnimationController _scaleController;
late Animation<double> _positionFactor; late Animation<double> _positionFactor;
late Animation<double> _scaleFactor;
late Animation<double> _value; late Animation<double> _value;
late Animation<Color?> _valueColor; late Animation<Color?> _valueColor;
RefreshIndicatorStatus? _status; RefreshIndicatorStatus? _status;
late Future<void> _pendingRefreshFuture; late Future<void> _pendingRefreshFuture;
bool? _isIndicatorAtTop;
double? _dragOffset; double? _dragOffset;
late Color _effectiveValueColor = late Color _effectiveValueColor =
widget.color ?? Theme.of(context).colorScheme.primary; widget.color ?? Theme.of(context).colorScheme.primary;
@@ -349,11 +229,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
end: _kDragSizeFactorLimit, end: _kDragSizeFactorLimit,
); );
static final Animatable<double> _oneToZeroTween = Tween<double>(
begin: 1.0,
end: 0.0,
);
@protected @protected
@override @override
void initState() { void initState() {
@@ -363,9 +238,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
// The "value" of the circular progress indicator during a drag. // The "value" of the circular progress indicator during a drag.
_value = _positionController.drive(_threeQuarterTween); _value = _positionController.drive(_threeQuarterTween);
_scaleController = AnimationController(vsync: this);
_scaleFactor = _scaleController.drive(_oneToZeroTween);
} }
@protected @protected
@@ -388,7 +260,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
@override @override
void dispose() { void dispose() {
_positionController.dispose(); _positionController.dispose();
_scaleController.dispose();
super.dispose(); super.dispose();
} }
@@ -417,77 +288,52 @@ class RefreshIndicatorState extends State<RefreshIndicator>
// If the notification.dragDetails is null, this scroll is not triggered by // 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. // 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. // In this case, we don't want to trigger the refresh indicator.
return ((notification is ScrollStartNotification && return (notification is ScrollStartNotification &&
notification.dragDetails != null) || notification.dragDetails != null) &&
(notification is ScrollUpdateNotification && notification.metrics.extentBefore == 0.0 &&
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 && _status == null &&
_start(notification.metrics.axisDirection); _start();
} }
double? _viewportDimension;
bool _handleScrollNotification(ScrollNotification notification) { bool _handleScrollNotification(ScrollNotification notification) {
if (!widget.notificationPredicate(notification)) { if (!widget.notificationPredicate(notification)) {
return false; return false;
} }
if (_shouldStart(notification)) { if (_shouldStart(notification)) {
_viewportDimension = notification.metrics.viewportDimension;
setState(() { setState(() {
_status = RefreshIndicatorStatus.drag; _status = RefreshIndicatorStatus.drag;
widget.onStatusChange?.call(_status);
}); });
return false; return false;
} }
final bool? indicatorAtTopNow = if (notification is ScrollUpdateNotification) {
switch (notification.metrics.axisDirection) { if (_status == RefreshIndicatorStatus.drag) {
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!; _dragOffset = _dragOffset! - notification.scrollDelta!;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.scrollDelta!;
}
_checkDragOffset(notification.metrics.viewportDimension); _checkDragOffset(notification.metrics.viewportDimension);
} }
if (_status == RefreshIndicatorStatus.armed && if (_status == RefreshIndicatorStatus.drag &&
notification.dragDetails == null) { notification.dragDetails == null &&
_valueColor.value!.a == _effectiveValueColor.a) {
// On iOS start the refresh when the Scrollable bounces back from the // On iOS start the refresh when the Scrollable bounces back from the
// overscroll (ScrollNotification indicating this don't have dragDetails // overscroll (ScrollNotification indicating this don't have dragDetails
// because the scroll activity is not directly triggered by a drag). // because the scroll activity is not directly triggered by a drag).
_show(); _show();
} }
} else if (notification is OverscrollNotification) { } else if (notification is OverscrollNotification) {
if (_status == RefreshIndicatorStatus.drag || if (_status == RefreshIndicatorStatus.drag) {
_status == RefreshIndicatorStatus.armed) {
if (notification.metrics.axisDirection == AxisDirection.down) {
_dragOffset = _dragOffset! - notification.overscroll; _dragOffset = _dragOffset! - notification.overscroll;
} else if (notification.metrics.axisDirection == AxisDirection.up) {
_dragOffset = _dragOffset! + notification.overscroll;
}
_checkDragOffset(notification.metrics.viewportDimension); _checkDragOffset(notification.metrics.viewportDimension);
} }
} else if (notification is ScrollEndNotification) { } else if (notification is ScrollEndNotification) {
switch (_status) { switch (_status) {
case RefreshIndicatorStatus.armed:
if (_positionController.value < 1.0) {
_dismiss(RefreshIndicatorStatus.canceled);
} else {
_show();
}
case RefreshIndicatorStatus.drag: case RefreshIndicatorStatus.drag:
if (_valueColor.value!.a == _effectiveValueColor.a) {
_show();
} else {
_dismiss(RefreshIndicatorStatus.canceled); _dismiss(RefreshIndicatorStatus.canceled);
}
case RefreshIndicatorStatus.canceled: case RefreshIndicatorStatus.canceled:
case RefreshIndicatorStatus.done: case RefreshIndicatorStatus.done:
case RefreshIndicatorStatus.refresh: case RefreshIndicatorStatus.refresh:
@@ -513,46 +359,25 @@ class RefreshIndicatorState extends State<RefreshIndicator>
return false; return false;
} }
bool _start(AxisDirection direction) { bool _start() {
assert(_status == null); assert(_status == null);
assert(_isIndicatorAtTop == null);
assert(_dragOffset == 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; _dragOffset = 0.0;
_scaleController.value = 0.0;
_positionController.value = 0.0; _positionController.value = 0.0;
return true; return true;
} }
void _checkDragOffset(double containerExtent) { void _checkDragOffset(double containerExtent) {
assert( assert(
_status == RefreshIndicatorStatus.drag || _status == RefreshIndicatorStatus.drag,
_status == RefreshIndicatorStatus.armed,
); );
double newValue = double newValue =
_dragOffset! / (containerExtent * kDragContainerExtentPercentage); _dragOffset! / (containerExtent * kDragContainerExtentPercentage);
if (_status == RefreshIndicatorStatus.armed) {
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
}
_positionController.value = clampDouble( _positionController.value = clampDouble(
newValue, newValue,
0.0, 0.0,
1.0, 1.0,
); // This triggers various rebuilds. ); // 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. // Stop showing the refresh indicator.
@@ -567,20 +392,15 @@ class RefreshIndicatorState extends State<RefreshIndicator>
); );
setState(() { setState(() {
_status = newMode; _status = newMode;
widget.onStatusChange?.call(_status);
}); });
switch (_status!) { switch (_status!) {
case RefreshIndicatorStatus.done: case RefreshIndicatorStatus.done:
await _scaleController.animateTo( break;
1.0,
duration: _kIndicatorScaleDuration,
);
case RefreshIndicatorStatus.canceled: case RefreshIndicatorStatus.canceled:
await _positionController.animateTo( await _positionController.animateTo(
0.0, 0.0,
duration: _kIndicatorScaleDuration, duration: _kIndicatorScaleDuration,
); );
case RefreshIndicatorStatus.armed:
case RefreshIndicatorStatus.drag: case RefreshIndicatorStatus.drag:
case RefreshIndicatorStatus.refresh: case RefreshIndicatorStatus.refresh:
case RefreshIndicatorStatus.snap: case RefreshIndicatorStatus.snap:
@@ -588,7 +408,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
} }
if (mounted && _status == newMode) { if (mounted && _status == newMode) {
_dragOffset = null; _dragOffset = null;
_isIndicatorAtTop = null;
setState(() { setState(() {
_status = null; _status = null;
}); });
@@ -601,7 +420,6 @@ class RefreshIndicatorState extends State<RefreshIndicator>
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
_pendingRefreshFuture = completer.future; _pendingRefreshFuture = completer.future;
_status = RefreshIndicatorStatus.snap; _status = RefreshIndicatorStatus.snap;
widget.onStatusChange?.call(_status);
_positionController _positionController
.animateTo( .animateTo(
1.0 / _kDragSizeFactorLimit, 1.0 / _kDragSizeFactorLimit,
@@ -640,11 +458,11 @@ class RefreshIndicatorState extends State<RefreshIndicator>
/// When initiated in this manner, the refresh indicator is independent of any /// 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 /// actual scroll view. It defaults to showing the indicator at the top. To
/// show it at the bottom, set `atTop` to false. /// show it at the bottom, set `atTop` to false.
Future<void> show({bool atTop = true}) { Future<void> show() {
if (_status != RefreshIndicatorStatus.refresh && if (_status != RefreshIndicatorStatus.refresh &&
_status != RefreshIndicatorStatus.snap) { _status != RefreshIndicatorStatus.snap) {
if (_status == null) { if (_status == null) {
_start(atTop ? AxisDirection.down : AxisDirection.up); _start();
} }
_show(); _show();
} }
@@ -655,7 +473,7 @@ class RefreshIndicatorState extends State<RefreshIndicator>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context)); assert(debugCheckHasMaterialLocalizations(context));
final Widget child = NotificationListener<ScrollNotification>( Widget child = NotificationListener<ScrollNotification>(
onNotification: _handleScrollNotification, onNotification: _handleScrollNotification,
child: NotificationListener<OverscrollIndicatorNotification>( child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: _handleIndicatorNotification, onNotification: _handleIndicatorNotification,
@@ -665,10 +483,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
assert(() { assert(() {
if (_status == null) { if (_status == null) {
assert(_dragOffset == null); assert(_dragOffset == null);
assert(_isIndicatorAtTop == null);
} else { } else {
assert(_dragOffset != null); assert(_dragOffset != null);
assert(_isIndicatorAtTop != null);
} }
return true; return true;
}()); }());
@@ -677,75 +493,30 @@ class RefreshIndicatorState extends State<RefreshIndicator>
_status == RefreshIndicatorStatus.refresh || _status == RefreshIndicatorStatus.refresh ||
_status == RefreshIndicatorStatus.done; _status == RefreshIndicatorStatus.done;
return Stack( child = Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: <Widget>[ children: <Widget>[
child, child,
if (_status != null) if (_status != null)
Positioned( Positioned(
top: _isIndicatorAtTop! ? widget.edgeOffset : null, top: widget.edgeOffset,
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
left: 0.0, left: 0.0,
right: 0.0, right: 0.0,
child: SizeTransition( child: SizeTransition(
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0, axisAlignment: 1.0,
sizeFactor: _positionFactor, // This is what brings it down. sizeFactor: _positionFactor, // This is what brings it down.
child: Padding( child: Padding(
padding: _isIndicatorAtTop! padding: EdgeInsets.only(top: widget.displacement),
? EdgeInsets.only(top: widget.displacement)
: EdgeInsets.only(bottom: widget.displacement),
child: Align( child: Align(
alignment: _isIndicatorAtTop! alignment: Alignment.topCenter,
? Alignment.topCenter
: Alignment.bottomCenter,
child: ScaleTransition(
scale: _scaleFactor,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _positionController, animation: _positionController,
builder: (BuildContext context, Widget? child) { builder: (context, child) => RefreshProgressIndicator(
final Widget materialIndicator = value: showIndeterminateIndicator ? null : _value.value,
RefreshProgressIndicator(
semanticsLabel:
widget.semanticsLabel ??
MaterialLocalizations.of(
context,
).refreshIndicatorSemanticLabel,
semanticsValue: widget.semanticsValue,
value: showIndeterminateIndicator
? null
: _value.value,
valueColor: _valueColor, valueColor: _valueColor,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
strokeWidth: widget.strokeWidth, strokeWidth: widget.strokeWidth,
elevation: widget.elevation, 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();
}
},
), ),
), ),
), ),
@@ -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({ Widget refreshIndicator({
required RefreshCallback onRefresh, required RefreshCallback onRefresh,
required Widget child, required Widget child,
bool isClampingScrollPhysics = false,
}) { }) {
return RefreshIndicator( return RefreshIndicator(
displacement: displacement, displacement: displacement,
onRefresh: onRefresh, onRefresh: onRefresh,
isClampingScrollPhysics: isClampingScrollPhysics,
child: child, 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;
}
}

View File

@@ -236,6 +236,7 @@ class _AudioPageState extends State<AudioPage> {
), ),
child: refreshIndicator( child: refreshIndicator(
onRefresh: () => _controller.loadPrev(context), onRefresh: () => _controller.loadPrev(context),
isClampingScrollPhysics: true,
child: CustomScrollView( child: CustomScrollView(
controller: scrollController, controller: scrollController,
physics: _controller.reachStart physics: _controller.reachStart

View File

@@ -12,7 +12,7 @@ import 'package:PiliPlus/models_new/video/video_detail/episode.dart';
import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart';
import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/material.dart' hide RefreshCallback; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';

View File

@@ -73,6 +73,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
}, },
child: refreshIndicator( child: refreshIndicator(
onRefresh: _videoReplyController.onRefresh, onRefresh: _videoReplyController.onRefresh,
isClampingScrollPhysics: widget.isNested,
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [

View File

@@ -181,6 +181,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
Widget buildList(ThemeData theme) { Widget buildList(ThemeData theme) {
final child = refreshIndicator( final child = refreshIndicator(
onRefresh: _controller.onRefresh, onRefresh: _controller.onRefresh,
isClampingScrollPhysics: widget.isNested,
child: CustomScrollView( child: CustomScrollView(
key: ValueKey(scrollController.hashCode), key: ValueKey(scrollController.hashCode),
controller: scrollController, controller: scrollController,