fix(player): 完善进度预览清屏与下拉刷新
This commit is contained in:
1 parent
9b1afbdb36
commit
f8500bbaca
15 files changed
+949
-251
No files matched your search
@@ -0,0 +1,180 @@
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user