feat(flutter): 统一文字下拉刷新

This commit is contained in:
Zhou Weidong committed 2026-10-20 16:47:13 +08:00
1 parent 0ad2cd5e95
commit 5bdcb989bb
19 files changed
+320 -38

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
// Author: fruit.com.cn
import 'dart:async';
import 'package:flutter/material.dart';
import '../../l10n/app_localizations.dart';
import '../theme/app_colors.dart';
const double _maxPullDistance = 96;
const double _refreshThreshold = 56;
const Duration _arrowTurnDuration = Duration(milliseconds: 160);
const Duration _retractDuration = Duration(milliseconds: 200);
enum _PullState { idle, pull, armed, refreshing }
/// 带文字状态提示的下拉刷新容器,指示区悬浮在内容上方。
class AppPullToRefresh extends StatefulWidget {
const AppPullToRefresh({
super.key,
required this.child,
required this.onRefresh,
this.edgeOffset = 0,
});
final Widget child;
final Future<void> Function() onRefresh;
final double edgeOffset;
@override
State<AppPullToRefresh> createState() => _AppPullToRefreshState();
}
class _AppPullToRefreshState extends State<AppPullToRefresh> {
_PullState _state = _PullState.idle;
double _pullDistance = 0;
bool _refreshing = false;
bool _onScroll(ScrollNotification notification) {
if (notification.depth != 0 || _refreshing) return false;
if (notification is ScrollStartNotification) {
if (notification.dragDetails != null) _setPullDistance(0);
} else if (notification is OverscrollNotification) {
if (notification.dragDetails != null) {
_setPullDistance(_pullDistance - notification.overscroll);
}
} else if (notification is ScrollEndNotification) {
_release();
}
return false;
}
void _setPullDistance(double value) {
final distance = value.clamp(0.0, _maxPullDistance);
final state = distance >= _refreshThreshold
? _PullState.armed
: distance > 0
? _PullState.pull
: _PullState.idle;
if (distance == _pullDistance && state == _state) return;
setState(() {
_pullDistance = distance;
_state = state;
});
}
void _release() {
if (_state == _PullState.armed) {
unawaited(_refresh());
} else if (_state == _PullState.pull) {
setState(() {
_pullDistance = 0;
_state = _PullState.idle;
});
}
}
Future<void> _refresh() async {
if (_refreshing) return;
_refreshing = true;
setState(() => _state = _PullState.refreshing);
try {
await widget.onRefresh();
} finally {
await Future<void>.delayed(_retractDuration);
if (mounted) {
setState(() {
_pullDistance = 0;
_state = _PullState.idle;
_refreshing = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: Stack(
fit: StackFit.expand,
children: [
widget.child,
if (_state != _PullState.idle)
Positioned(
top: widget.edgeOffset,
left: 0,
right: 0,
child: IgnorePointer(child: _indicator(context)),
),
],
),
);
}
Widget _indicator(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
if (_state == _PullState.refreshing) {
return _label(
const SizedBox.square(
dimension: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.blue,
),
),
l10n.refreshingLabel,
);
}
final armed = _state == _PullState.armed;
return _label(
AnimatedRotation(
turns: armed ? 0.5 : 0,
duration: _arrowTurnDuration,
child: Icon(
Icons.arrow_downward_rounded,
size: 18,
color: armed ? AppColors.blue : AppColors.muted,
),
),
armed ? l10n.releaseToRefresh : l10n.pullToRefresh,
emphasized: armed,
);
}
Widget _label(Widget icon, String text, {bool emphasized = false}) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
const SizedBox(width: 6),
Text(
text,
style: TextStyle(
color: emphasized ? AppColors.blue : AppColors.muted,
fontSize: 12,
fontWeight: emphasized ? FontWeight.w700 : FontWeight.w500,
),
),
],
);
}
}