在 Flutter 项目中,下拉刷新 / 上拉加载几乎是标配能力,而 easy_refresh 作为目前生态里最成熟、可定制性最高的刷新组件之一,在新版本中已经完全统一了 Header / Footer 的自定义方式,逻辑也更加清晰。
本文将从 原理 → 核心 API → 自定义 Header / Footer 实战 → 常见坑 几个维度,系统讲清楚 EasyRefresh 的自定义用法,适合你在业务项目、组件封装、博客输出中直接使用。
一、EasyRefresh 新版本整体结构
在新版本 easy_refresh 中:
- 刷新区域由 EasyRefresh 统一管理
- 下拉刷新:Header
- 上拉加载:Footer
- 状态通过 IndicatorState 驱动
基础使用代码示例:
dart
EasyRefresh(
header: ClassicHeader(),
footer: ClassicFooter(),
onRefresh: () async {},
onLoad: () async {},
child: ListView.builder(...),
)
如果你想做完全自定义的动画 / UI / 交互,核心就是:
自定义 Header / Footer = 自定义 Indicator
二、自定义 Header / Footer 的核心原理
1️⃣ IndicatorState(最核心)
IndicatorState 是 EasyRefresh 内部暴露的刷新状态快照,它包含当前刷新阶段、拖拽偏移量、动画值、是否正在刷新/加载等关键信息。
常用字段:
dart
state.mode // 当前模式
state.offset // 当前拖拽偏移
state.axis // 滚动方向
state.result // 刷新结果
2️⃣ 刷新状态(mode)
常见的 IndicatorMode 状态如下:
| mode | 含义 |
|---|---|
| inactive | 未激活 |
| drag | 拖拽中 |
| armed | 达到触发阈值 |
| processing | 正在刷新/加载 |
| processed | 刷新完成 |
| done | 结束 |
自定义 Header / Footer 的本质 :根据 mode + offset 构建不同的 UI 样式。
三、最推荐的方式:BuilderHeader / BuilderFooter
EasyRefresh 提供了 BuilderHeader / BuilderFooter 这两个工具类,是官方最推荐的自定义方式,无需继承复杂类,直接通过 Builder 构建 UI。
1️⃣ 自定义 Header 示例
效果目标
- 下拉时显示箭头
- 超过触发高度自动旋转
- 刷新时显示 loading
实现代码
dart
BuilderHeader(
triggerOffset: 70, // 触发刷新的偏移量
clamping: true,
position: IndicatorPosition.above,
builder: (context, state) {
return SizedBox(
height: state.offset,
child: Center(
child: _buildHeaderContent(state),
),
);
},
)
// Header 内容构建方法
Widget _buildHeaderContent(IndicatorState state) {
if (state.mode == IndicatorMode.processing) {
return const CircularProgressIndicator(strokeWidth: 2);
}
// 根据偏移量计算旋转角度
final rotate = state.offset / 70;
return Transform.rotate(
angle: rotate * 3.14,
child: const Icon(Icons.arrow_downward),
);
}
2️⃣ 自定义 Footer 示例
Footer 的自定义逻辑和 Header 完全一致,只是方向相反。
实现代码
dart
BuilderFooter(
triggerOffset: 60,
clamping: true,
position: IndicatorPosition.below,
builder: (context, state) {
return SizedBox(
height: state.offset,
child: Center(
child: _buildFooterContent(state),
),
);
},
)
// Footer 内容构建方法
Widget _buildFooterContent(IndicatorState state) {
switch (state.mode) {
case IndicatorMode.processing:
return const CircularProgressIndicator(strokeWidth: 2);
default:
return const Text('上拉加载更多');
}
}
四、完整使用示例(可直接运行)
dart
import 'package:flutter/material.dart';
import 'package:easy_refresh/easy_refresh.dart';
class EasyRefreshDemo extends StatelessWidget {
const EasyRefreshDemo({super.key});
@override
Widget build(BuildContext context) {
return EasyRefresh(
header: BuilderHeader(
triggerOffset: 70,
clamping: true,
position: IndicatorPosition.above,
builder: (context, state) {
return SizedBox(
height: state.offset,
child: Center(
child: _buildHeaderContent(state),
),
);
},
),
footer: BuilderFooter(
triggerOffset: 60,
clamping: true,
position: IndicatorPosition.below,
builder: (context, state) {
return SizedBox(
height: state.offset,
child: Center(
child: _buildFooterContent(state),
),
);
},
),
onRefresh: () async {
// 模拟刷新请求
await Future.delayed(const Duration(seconds: 1));
},
onLoad: () async {
// 模拟加载请求
await Future.delayed(const Duration(seconds: 1));
},
child: ListView.builder(
itemCount: 20,
itemBuilder: (_, index) => ListTile(title: Text('Item $index')),
),
);
}
Widget _buildHeaderContent(IndicatorState state) {
if (state.mode == IndicatorMode.processing) {
return const CircularProgressIndicator(strokeWidth: 2);
}
final rotate = state.offset / 70;
return Transform.rotate(
angle: rotate * 3.14,
child: const Icon(Icons.arrow_downward),
);
}
Widget _buildFooterContent(IndicatorState state) {
switch (state.mode) {
case IndicatorMode.processing:
return const CircularProgressIndicator(strokeWidth: 2);
default:
return const Text('上拉加载更多');
}
}
}
五、进阶技巧(非常实用)
✅ 1. 固定高度 Header
如果需要使用 Lottie 动画或复杂固定布局的 Header,可以直接固定高度,不依赖 state.offset:
dart
BuilderHeader(
triggerOffset: 80,
builder: (context, state) {
return const SizedBox(
height: 80, // 固定高度
child: Center(
child: Lottie.asset('assets/refresh.json'), // Lottie 动画示例
),
);
},
)
✅ 2. 根据 offset 做渐变 / 缩放
利用 state.offset 可以实现渐变显示、缩放等过渡效果:
dart
Widget _buildHeaderContent(IndicatorState state) {
// 计算进度,限制在 0-1 之间
final progress = (state.offset / 80).clamp(0.0, 1.0);
return Opacity(
opacity: progress, // 透明度随偏移量变化
child: Transform.scale(
scale: progress, // 缩放随偏移量变化
child: const Icon(Icons.refresh),
),
);
}
✅ 3. 业务封装(强烈推荐)
将自定义 Header/Footer 封装成独立组件,便于项目统一风格:
dart
class CommonRefreshHeader extends BuilderHeader {
CommonRefreshHeader()
: super(
triggerOffset: 70,
clamping: true,
position: IndicatorPosition.above,
builder: (context, state) {
return SizedBox(
height: state.offset,
child: Center(
child: state.mode == IndicatorMode.processing
? const CircularProgressIndicator(strokeWidth: 2)
: Transform.rotate(
angle: (state.offset / 70) * 3.14,
child: const Icon(Icons.arrow_downward),
),
),
);
},
);
}
// 使用时直接调用
EasyRefresh(
header: CommonRefreshHeader(),
// ...
)
六、常见坑总结
❌ 1. Header 不显示
- 忘记设置
onRefresh回调方法 - ListView 没有可滚动高度(比如内容不足一屏,可添加
physics: AlwaysScrollableScrollPhysics())
❌ 2. offset 一直为 0
clamping参数设置错误,需要根据需求调整- 外层父组件设置了
NeverScrollableScrollPhysics,导致无法触发拖拽
❌ 3. 动画卡顿
- 在 builder 中频繁创建新对象(比如每次都 new Icon),可以抽成常量
- 避免在 builder 中执行复杂计算,建议提前缓存计算结果
七、使用场景选择
| 适用场景 | 推荐方案 |
|---|---|
| 产品有强视觉要求 | 自定义 BuilderHeader/BuilderFooter |
| 需要品牌化刷新动画 | 结合 Lottie 实现固定高度自定义 Header |
| 封装通用组件 | 封装成独立的 Header/Footer 类 |
| 普通列表页 | 直接使用 ClassicHeader/ClassicFooter |
八、总结
一句话总结 EasyRefresh 自定义的核心:
BuilderHeader / BuilderFooter + IndicatorState 状态驱动 UI
掌握这套思路后,无论是 Lottie 刷新动画、抖音式阻尼刷新,还是游戏化加载动效,都只是 UI 层面的实现,底层的刷新逻辑完全由 EasyRefresh 统一管理。