Flutter EasyRefresh 最新版本:自定义 Header / Footer 详解与实践

在 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


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),
  );
}

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 统一管理。

相关推荐
鹏程十八少2 小时前
Android ANR项目实战:Reason: Broadcast { act=android.intent.action.TIME_TICK}
android·前端·人工智能
Sheffi662 小时前
iOS 锁的本质:互斥锁、自旋锁、递归锁深度解析
ios
TheNextByte12 小时前
如何将Android中的照片传输到Windows 11/10
android·windows
徐安安ye2 小时前
Flutter AR 开发:打造厘米级精度的室内导航应用
flutter·ar
夜-未央2 小时前
iOS QQ分享报错:应用未正确授权(错误码:25105)
ios
大猫熊猫2 小时前
【ios】xcode运行项目时报错 Showing All Errors Only Framework ‘Pods_Runner‘ not found
macos·ios·xcode
2501_916008892 小时前
iPhone 耗电异常检测的思路,从系统电池统计、克魔(KeyMob)、Instruments等工具出发
android·ios·小程序·uni-app·cocoa·iphone·webview
撩得Android一次心动2 小时前
Android 四大组件——ContentProvider(内容提供者)
android·内容提供者·android 四大组件
2501_915921432 小时前
App Store 上架流程中常见的关键问题
android·ios·小程序·https·uni-app·iphone·webview