feat(flutter): 统一文字下拉刷新
This commit is contained in:
1 parent
0ad2cd5e95
commit
5bdcb989bb
19 files changed
+320
-38
No files matched your search
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user