初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/tools_base/refresh/pull_refresh.dart'; // 复用 CustomRefreshView 的手感调优
import 'package:pull_to_refresh/pull_to_refresh.dart';
/// 横向列表"拉到底查看更多"组件(手感同上拉加载更多,只是横向)。
///
/// 正常不占位不显示,滑到末尾继续拉才从右侧冒出 footer,
/// 拉过 [triggerDistance] 松手即触发 [onTrigger](跳页/加载等,由调用方决定)。
///
/// 触发以"松手瞬间是否仍过阈值"为准:拉出"释放查看"后又拖回阈值内再松手,不触发。
///
/// [child] 传横向滚动体(如 `scrollDirection: Axis.horizontal` 的 ListView)。
/// 内部自建/释放 RefreshController,并用独立 RefreshConfiguration 兜住过拉距离,
/// 嵌在竖向 SmartRefresher 里也不会被外层配置限制。
class HorizontalLoadMore extends StatefulWidget {
final Widget child;
final VoidCallback onTrigger; // 拉过阈值松手触发
final String idleText; // 未拉够文案
final String releaseText; // 拉够文案
final double triggerDistance; // 触发拉动距离(px)
final double footerWidth; // footer 宽度
const HorizontalLoadMore({
super.key,
required this.child,
required this.onTrigger,
this.idleText = '查看\n更多',
this.releaseText = '释放\n查看',
this.triggerDistance = 50,
this.footerWidth = 50,
});
@override
State<HorizontalLoadMore> createState() => _HorizontalLoadMoreState();
}
class _HorizontalLoadMoreState extends State<HorizontalLoadMore> {
final RefreshController _refreshCtr = RefreshController();
final ValueNotifier<bool> _armed =
ValueNotifier(false); // 当前过拉是否已过阈值(驱动 footer 文案 + 触发判定)
@override
void dispose() {
_refreshCtr.dispose();
_armed.dispose();
super.dispose();
}
// 拖动中持续更新是否已过阈值(公式同 SmartRefresher footer 的 offset 计算)
bool _onScroll(ScrollNotification n) {
if (n.metrics.axis != Axis.horizontal) return false;
final double overscroll =
n.metrics.pixels - n.metrics.maxScrollExtent; // 拉过末尾的距离
_armed.value = overscroll >= widget.triggerDistance;
return false;
}
// 手指抬起瞬间仍过阈值才触发(拖回阈值内松手不触发)。用原始指针事件最可靠,
// 不依赖 SmartRefresher 的 canLoading/onLoading(受滚动物理影响、拖回会锁死)
void _onPointerUp() {
if (_armed.value) {
_armed.value = false;
Future.microtask(widget.onTrigger); // 避免在事件派发中直接 push 路由
}
}
@override
Widget build(BuildContext context) {
return RefreshConfiguration(
// 手感对齐 CustomRefreshView:同一套回弹弹簧 + 拖拽速率 + 滚动行为
springDescription: kRefreshSpring,
dragSpeedRatio: kRefreshDragSpeedRatio,
maxOverScrollExtent: 60,
maxUnderScrollExtent: 150, // 竖向那套是0,横向靠拖动触发需给足距离
enableBallisticLoad: false, // 不靠惯性,只松手触发
footerTriggerDistance: widget.triggerDistance,
child: ScrollConfiguration(
behavior: NoStretchScrollBehavior(), // BouncingPhysics/去拉伸,同竖向
child: Listener(
onPointerUp: (_) => _onPointerUp(),
child: NotificationListener<ScrollNotification>(
onNotification: _onScroll,
child: SmartRefresher(
controller: _refreshCtr,
scrollDirection: Axis.horizontal,
enablePullDown: false,
enablePullUp: true,
footer: CustomFooter(
// 正常不占位不显示,只有滑到末尾继续拉才冒出来
loadStyle: LoadStyle.HideAlways,
builder: (_, __) => ValueListenableBuilder<bool>(
valueListenable: _armed,
builder: (_, armed, ___) => _footer(armed),
),
),
onLoading: () =>
_refreshCtr.loadComplete(), // 触发已由指针事件把关,这里只复位 footer
child: widget.child,
),
),
),
),
);
}
// 过阈值(armed)时变"释放"文案、箭头翻转
Widget _footer(bool armed) {
return Container(
width: widget.footerWidth,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedRotation(
turns: armed ? 0.5 : 0,
duration: const Duration(milliseconds: 200),
child:
Image.asset("arrow_right_grey.webp".commonImgPath, width: 18),
),
const SizedBox(height: 6),
Text(
armed ? widget.releaseText : widget.idleText,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0x59FFFFFF), fontSize: 10, height: 1.3),
),
],
),
);
}
}
+179
View File
@@ -0,0 +1,179 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
// 回弹弹簧:临界阻尼(≈2√(stiffness·mass)=34.6) → 松手快速收回不过冲
// 注:实测此调优手感优于 3.16.8 包默认(mass2.2/stiffness150/damping16,欠阻尼、回弹更夸张),
// 故采用这套调优值,不复刻 3.16.8 原值
// 提成常量供横向的 HorizontalLoadMore 复用,两处手感必须一致,改这里即全改
const SpringDescription kRefreshSpring =
SpringDescription(mass: 1.5, stiffness: 200, damping: 35);
const double kRefreshDragSpeedRatio = 0.91;
Widget pullYsRefresh({
required RefreshViewOnInit onInit,
required Widget child,
Key? key,
final Function(RefreshController? ctr)? onRefresh,
final Function(RefreshController? ctr)? onLoading,
bool enablePullUp = true,
bool enablePullDown = true,
String noDataText = "—— 没有更多数据了 ——",
Widget? customHeader,
Widget? footerHeader,
}) {
return CustomRefreshView(
onInit: onInit,
onLoading: onLoading,
onRefresh: onRefresh,
customHeader: customHeader,
footerHeader: footerHeader,
enablePullDown: enablePullDown,
enablePullUp: enablePullUp,
noDataText: noDataText,
child: child,
);
}
typedef RefreshViewOnInit = Function(RefreshController controller);
class CustomRefreshView extends StatefulWidget {
final bool enablePullDown;
final bool enablePullUp;
final Function(RefreshController? ctr)? onRefresh;
final Function(RefreshController? ctr)? onLoading;
final Widget? customHeader;
final Widget? footerHeader;
final Widget child;
final String noDataText;
final RefreshViewOnInit onInit;
const CustomRefreshView({
super.key,
required this.child,
required this.onInit,
this.enablePullDown = true,
this.enablePullUp = true,
this.onRefresh,
this.onLoading,
this.customHeader,
this.footerHeader,
this.noDataText = "—— 没有更多数据了 ——",
});
@override
State<CustomRefreshView> createState() => _CustomIRefreshViewState();
}
class _CustomIRefreshViewState extends State<CustomRefreshView> {
final refreshCtr = RefreshController();
@override
void initState() {
widget.onInit(refreshCtr);
super.initState();
}
@override
void dispose() {
//组件创建的 controller 由组件释放(谁创建谁释放)
refreshCtr.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RefreshConfiguration(
springDescription: kRefreshSpring,
dragSpeedRatio: kRefreshDragSpeedRatio,
maxOverScrollExtent: 60,
maxUnderScrollExtent: 0,
enableBallisticRefresh: false, // 下拉不靠惯性触发刷新
enableBallisticLoad: true, // 上拉滑到底自动加载
//去掉 StretchingOverscrollIndicator,避免与下拉刷新冲突
child: ScrollConfiguration(
behavior: NoStretchScrollBehavior(),
child: SmartRefresher(
enablePullDown: widget.enablePullDown,
enablePullUp: widget.enablePullUp,
controller: refreshCtr,
onRefresh: () => widget.onRefresh?.call(refreshCtr),
onLoading: () => widget.onLoading?.call(refreshCtr),
header: widget.customHeader ??
WaterDropHeader(
waterDropColor: AppColors.actionRed,
// iOS 刷新菊花用主题色;Android 保持默认 loading
refresh: Platform.isIOS
? const CupertinoActivityIndicator(
color: AppColors.actionRed)
: null,
complete: const Text(
"刷新完成!",
style: TextStyle(
color: AppColors.tipTextColor99, fontSize: 12),
),
failed: const Text(
"刷新失敗!",
style: TextStyle(
color: AppColors.tipTextColor99, fontSize: 12),
),
),
footer: widget.footerHeader ??
ClassicFooter(
loadingText: "加载中...",
canLoadingText: "松开加载更多...",
noDataText: widget.noDataText,
idleText: "上拉加载更多",
// Android 默认转圈是蓝色,改成主题色;iOS 保持菊花
loadingIcon: SizedBox(
width: 25,
height: 25,
child: Platform.isIOS
? const CupertinoActivityIndicator()
: const CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation(AppColors.actionRed),
),
),
textStyle: TextStyle(
color: AppColors.tipTextColor99,
fontSize: 12,
decoration: TextDecoration.none,
fontWeight: FontWeight.normal,
),
),
child: widget.child),
),
);
}
}
//还原 Flutter 3.16 时代的滚动行为
class NoStretchScrollBehavior extends MaterialScrollBehavior {
//去掉 StretchingOverscrollIndicator,避免与下拉刷新冲突
@override
Widget buildOverscrollIndicator(
BuildContext context,
Widget child,
ScrollableDetails details,
) {
return child;
}
//还原旧版多指拖拽策略(3.19+ 改为 latestPointer,拖拽手感有细微差异)
@override
MultitouchDragStrategy getMultitouchDragStrategy(BuildContext context) {
return MultitouchDragStrategy.averageBoundaryPointers;
}
//使用 BouncingScrollPhysics 还原弹性下拉手感
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
return const BouncingScrollPhysics();
}
}