Files
ReelShort/lib/pages/player/widgets/feed_pull_refresh.dart

181 lines
5.0 KiB
Dart

// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
const double _maxPull = 96;
const double _refreshThreshold = 56;
const double _firstPageEpsilon = 0.01;
/// 第一条已经到顶时,向下的拖动不再翻页。
class FeedRefreshPhysics extends PageScrollPhysics {
const FeedRefreshPhysics({super.parent});
@override
FeedRefreshPhysics applyTo(ScrollPhysics? ancestor) {
return FeedRefreshPhysics(parent: buildParent(ancestor));
}
@override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (position.pixels <= position.minScrollExtent && offset > 0) {
return 0;
}
return super.applyPhysicsToUserOffset(position, offset);
}
@override
Simulation? createBallisticSimulation(
ScrollMetrics position,
double velocity,
) {
if (position.pixels <= position.minScrollExtent && velocity < 0) {
return super.createBallisticSimulation(position, 0);
}
return super.createBallisticSimulation(position, velocity);
}
}
/// 竖滑到第一条后继续下拉时刷新,其它页仍交给翻页。
class FeedPullRefresh extends StatefulWidget {
const FeedPullRefresh({
super.key,
required this.pageController,
required this.onRefresh,
required this.child,
this.topInset = 0,
});
final PageController pageController;
final Future<void> Function() onRefresh;
final Widget child;
final double topInset;
@override
State<FeedPullRefresh> createState() => _FeedPullRefreshState();
}
class _FeedPullRefreshState extends State<FeedPullRefresh> {
Offset? _origin;
double _pull = 0;
bool _tracking = false;
bool _refreshing = false;
bool _atFirstPage() {
final controller = widget.pageController;
if (!controller.hasClients) return false;
return (controller.page ?? 0) <= _firstPageEpsilon;
}
void _setPull(double value) {
final next = value.clamp(0.0, _maxPull);
if (next == _pull) return;
setState(() => _pull = next);
}
void _onPointerDown(PointerDownEvent event) {
if (_refreshing || !_atFirstPage()) {
_tracking = false;
return;
}
_origin = event.position;
_tracking = true;
}
void _onPointerMove(PointerMoveEvent event) {
final origin = _origin;
if (!_tracking || origin == null || _refreshing) return;
final delta = event.position - origin;
if (delta.dx.abs() > delta.dy.abs() && delta.dy < _refreshThreshold) {
_tracking = false;
_setPull(0);
return;
}
if (delta.dy > 0) _holdFirstPage();
_setPull(delta.dy);
}
void _holdFirstPage() {
final controller = widget.pageController;
if (!controller.hasClients) return;
final position = controller.position;
if (position.pixels <= position.minScrollExtent) return;
controller.jumpTo(position.minScrollExtent);
}
void _onPointerEnd() {
final shouldRefresh = _tracking && _pull >= _refreshThreshold;
_origin = null;
_tracking = false;
if (shouldRefresh) {
_refresh();
return;
}
_setPull(0);
}
Future<void> _refresh() async {
if (_refreshing) return;
_refreshing = true;
_setPull(_refreshThreshold);
try {
await widget.onRefresh();
} finally {
_refreshing = false;
if (mounted) _setPull(0);
}
}
@override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _onPointerDown,
onPointerMove: _onPointerMove,
onPointerUp: (_) => _onPointerEnd(),
onPointerCancel: (_) => _onPointerEnd(),
child: Stack(
fit: StackFit.expand,
children: [
widget.child,
if (_pull > 0)
Positioned(
top: widget.topInset,
left: 0,
right: 0,
child: IgnorePointer(
child: Align(
alignment: Alignment.topCenter,
child: Container(
key: const Key('pull-refresh-indicator'),
width: AppDimens.refreshIndicatorSize,
height: AppDimens.refreshIndicatorSize,
padding: const EdgeInsets.all(
(AppDimens.refreshIndicatorSize -
AppDimens.refreshSpinnerSize) /
2,
),
decoration: BoxDecoration(
color: AppColors.refreshIndicatorSurface,
borderRadius: BorderRadius.circular(
AppDimens.refreshIndicatorRadius,
),
),
child: const SizedBox.square(
dimension: AppDimens.refreshSpinnerSize,
child: CircularProgressIndicator(
strokeWidth: AppDimens.refreshSpinnerStroke,
color: AppColors.textPrimary,
),
),
),
),
),
),
],
),
);
}
}