style: 统一下拉与加载指示器

This commit is contained in:
Zhou Weidong committed 2026-10-22 22:07:13 +08:00
1 parent 397483d339
commit 6f1da5aa39
26 files changed
+127 -241

No files matched your search

+22 -48
View File
@@ -4,19 +4,18 @@ import 'dart:async';
import 'package:flutter/material.dart';
import '../../l10n/app_localizations.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
const double _maxPullDistance = 96;
const double _refreshThreshold = 56;
const double _indicatorHeight = 56;
const double _indicatorDisplacement = 40;
const Duration _arrowTurnDuration = Duration(milliseconds: 160);
const Duration _settleDuration = Duration(milliseconds: 200);
enum _PullState { idle, pull, armed, refreshing }
/// 带文字状态提示的下拉刷新容器,指示区悬浮在内容上方。
/// 下拉刷新容器,指示区只显示统一的白色加载圈。
class AppPullToRefresh extends StatefulWidget {
const AppPullToRefresh({
super.key,
@@ -172,7 +171,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
top: widget.edgeOffset,
left: 0,
right: 0,
child: IgnorePointer(child: _indicatorLayer(context, offset)),
child: IgnorePointer(child: _indicatorLayer(offset)),
),
],
);
@@ -181,7 +180,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
);
}
Widget _indicatorLayer(BuildContext context, double offset) {
Widget _indicatorLayer(double offset) {
final progress = (offset / _refreshThreshold).clamp(0.0, 1.0);
return SizedBox(
height: _indicatorHeight,
@@ -193,7 +192,7 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: _indicator(context),
child: _indicator(),
),
),
),
@@ -201,50 +200,25 @@ class _AppPullToRefreshState extends State<AppPullToRefresh>
);
}
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.navSelected,
),
),
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: AppColors.navSelected,
Widget _indicator() {
return 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,
),
),
armed ? l10n.releaseToRefresh : l10n.pullToRefresh,
);
}
Widget _label(Widget icon, String text) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
icon,
const SizedBox(width: 6),
Text(
text,
style: const TextStyle(
color: AppColors.navSelected,
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
],
);
}
}
+7 -30
View File
@@ -5,22 +5,19 @@ import 'dart:async';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
import '../../l10n/app_localizations.dart';
/// 网络分页列表底部状态:加载、重试、手动加载与无更多。
/// 网络分页列表底部状态:仅保留加载圈与错误重试。
class PagedListFooter extends StatelessWidget {
const PagedListFooter({
super.key,
required this.loading,
required this.hasMore,
required this.hasItems,
required this.onRetry,
this.errorMessage,
});
final bool loading;
final bool hasMore;
final bool hasItems;
final String? errorMessage;
final Future<void> Function() onRetry;
@@ -49,38 +46,18 @@ class PagedListFooter extends StatelessWidget {
if (loading) {
return const SizedBox(
key: Key('paged-list-loading'),
height: 56,
height: AppDimens.loadMoreIndicatorHeight,
child: Center(
child: SizedBox.square(
dimension: 20,
dimension: AppDimens.refreshSpinnerSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.blue,
strokeWidth: AppDimens.refreshSpinnerStroke,
color: AppColors.textPrimary,
),
),
),
);
}
if (hasMore) {
return Center(
child: TextButton(
key: const Key('paged-list-load-more'),
onPressed: () => unawaited(onRetry()),
child: Text(l10n.loadMore),
),
);
}
if (hasItems) {
return Padding(
key: const Key('paged-list-no-more'),
padding: const EdgeInsets.symmetric(vertical: 14),
child: Text(
l10n.noMore,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
);
}
return const SizedBox(height: 16);
return const SizedBox.shrink();
}
}
+7 -25
View File
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_dimens.dart';
const int _defaultPageSize = 20;
const Duration _defaultLoadDelay = Duration(milliseconds: 300);
@@ -64,53 +65,34 @@ class PagedLocalListController<T> extends ChangeNotifier {
}
}
/// 分页列表底部加载态与到底提示。
/// 分页列表底部加载态。
class PagedLoadFooter extends StatelessWidget {
const PagedLoadFooter({
super.key,
required this.loading,
required this.hasMore,
required this.hasItems,
required this.loadMoreKey,
required this.noMoreKey,
required this.noMoreLabel,
});
final bool loading;
final bool hasMore;
final bool hasItems;
final Key loadMoreKey;
final Key noMoreKey;
final String noMoreLabel;
@override
Widget build(BuildContext context) {
if (loading) {
return SizedBox(
key: loadMoreKey,
height: 56,
height: AppDimens.loadMoreIndicatorHeight,
child: const Center(
child: SizedBox.square(
dimension: 20,
dimension: AppDimens.refreshSpinnerSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.blue,
strokeWidth: AppDimens.refreshSpinnerStroke,
color: AppColors.textPrimary,
),
),
),
);
}
if (hasItems && !hasMore) {
return Padding(
key: noMoreKey,
padding: const EdgeInsets.symmetric(vertical: 14),
child: Text(
noMoreLabel,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.muted, fontSize: 12),
),
);
}
return const SizedBox(height: 16);
return const SizedBox.shrink();
}
}