| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- import 'dart:async';
- import 'package:flutter/material.dart'
- hide RefreshIndicatorState, RefreshIndicator;
- import 'package:flutter_svg/flutter_svg.dart';
- import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
- import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart';
- import '../constants/assets.dart';
- /// 自定义渐变圆形到圆柱形刷新头部
- class GradientCircleHeader extends RefreshIndicator {
- /// refreshing content
- final Widget? refresh;
- /// complete content
- final Widget? complete;
- /// failed content
- final Widget? failed;
- const GradientCircleHeader({
- super.key,
- this.refresh,
- this.complete,
- super.completeDuration = const Duration(milliseconds: 600),
- this.failed,
- }) : super(height: 60.0, refreshStyle: RefreshStyle.UnFollow);
- @override
- State<StatefulWidget> createState() {
- return _GradientCircleHeaderState();
- }
- }
- class _GradientCircleHeaderState
- extends RefreshIndicatorState<GradientCircleHeader>
- with TickerProviderStateMixin {
- AnimationController? _animationController;
- late AnimationController _dismissCtl;
- late AnimationController _rotateCtl; // 旋转动画控制器
- late AnimationController _colorCtl; // 颜色过渡动画控制器
- double _currentOffset = 0.0; // 跟踪当前下拉距离
- bool _svgsPrecached = false; // SVG 是否已预加载
- // 默认状态的背景渐变
- final Color _defaultBgStartColor = const Color(0xA6BABABA);
- final Color _defaultBgEndColor = const Color(0x33404040);
- // 默认状态的边框渐变
- final List<Color> _defaultBorderColors = const [
- Color(0xFFDFDFDF),
- Color(0xFFA2A2A2),
- Color(0x4F6B6B6B),
- ];
- // 当前状态的背景渐变颜色
- Color _currentBgStartColor = const Color(0xA6BABABA);
- Color _currentBgEndColor = const Color(0x33404040);
- // 当前状态的边框渐变颜色
- List<Color> _currentBorderColors = [
- const Color(0xFFDFDFDF),
- const Color(0xFFA2A2A2),
- const Color(0x4F6B6B6B),
- ];
- // 成功状态的背景渐变
- final Color _successBgStartColor = const Color(0xFF0D2116);
- final Color _successBgEndColor = const Color(0xFF228643);
- // 成功状态的边框渐变
- final List<Color> _successBorderColors = [
- const Color(0xFF253B2E),
- const Color(0xFF41905A),
- const Color(0xFF7BF4AB),
- ];
- // 失败状态的背景渐变
- final Color _failedBgStartColor = const Color(0xFF1E1215);
- final Color _failedBgEndColor = const Color(0xFFAA4140);
- // 失败状态的边框渐变
- final List<Color> _failedBorderColors = [
- const Color(0xFF4B3033),
- const Color(0xFFDC7A7F),
- const Color(0xFFFFBDBB),
- ];
- @override
- void onOffsetChange(double offset) {
- // 圆形完全显示需要的距离:topMargin(12) + diameter(30) = 42
- const double circleFullyVisibleOffset = 42.0;
- // 更新当前下拉距离(但在特定状态下固定为完整圆形的距离)
- if (mounted) {
- setState(() {
- // 如果正在刷新、成功、失败状态,固定 offset 为完整圆形的距离
- if (mode == RefreshStatus.refreshing ||
- mode == RefreshStatus.completed ||
- mode == RefreshStatus.failed) {
- _currentOffset = circleFullyVisibleOffset;
- } else {
- _currentOffset = offset;
- }
- });
- }
- // 如果正在刷新、成功、失败状态,保持圆形状态但不强制设置值
- // 让 readyToRefresh 的动画自然完成
- if (mode == RefreshStatus.refreshing ||
- mode == RefreshStatus.completed ||
- mode == RefreshStatus.failed) {
- // 不在这里强制设置值,避免打断动画造成闪烁
- return;
- }
- // 如果动画正在执行(收缩动画),不打断它
- if (_animationController!.isAnimating) {
- return;
- }
- // 跟随 offset 变化
- if (offset <= circleFullyVisibleOffset) {
- // 第一阶段:圆形逐渐显示,不变形
- _animationController!.value = 0.0;
- } else {
- // 第二阶段:圆形完全显示后,继续下拉变成圆柱
- final double cylinderOffset = offset - circleFullyVisibleOffset;
- _animationController!.value = cylinderOffset.clamp(0.0, 50.0);
- }
- }
- @override
- Future<void> readyToRefresh() {
- // 使用短时间的快速动画收缩到圆形
- return _animationController!.animateTo(
- 0.0,
- duration: Duration(milliseconds: 150), // 快速收缩
- curve: Curves.easeOut,
- );
- }
- @override
- void initState() {
- _dismissCtl = AnimationController(
- vsync: this,
- duration: Duration(milliseconds: 400),
- value: 1.0,
- );
- _animationController = AnimationController(
- vsync: this,
- lowerBound: 0.0,
- upperBound: 50.0,
- duration: Duration(milliseconds: 150), // 缩短动画时长以匹配快速回弹
- );
- _rotateCtl = AnimationController(
- vsync: this,
- duration: Duration(milliseconds: 1000),
- );
- _colorCtl = AnimationController(
- vsync: this,
- duration: Duration(milliseconds: 300),
- );
- super.initState();
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- // 预加载 SVG 图标,避免首次显示时闪烁
- _precacheSvgs();
- }
- /// 预加载所有状态使用的 SVG 图标
- void _precacheSvgs() {
- if (_svgsPrecached) return;
- _svgsPrecached = true;
- // 预缓存刷新状态的 SVG
- final refreshLoader = SvgAssetLoader(Assets.refreshCircle);
- svg.cache.putIfAbsent(
- refreshLoader.cacheKey(null),
- () => refreshLoader.loadBytes(null),
- );
- // 预缓存成功状态的 SVG
- final successLoader = SvgAssetLoader(Assets.successCircle);
- svg.cache.putIfAbsent(
- successLoader.cacheKey(null),
- () => successLoader.loadBytes(null),
- );
- // 预缓存失败状态的 SVG
- final failedLoader = SvgAssetLoader(Assets.failedCircle);
- svg.cache.putIfAbsent(
- failedLoader.cacheKey(null),
- () => failedLoader.loadBytes(null),
- );
- // 预缓存下拉箭头的 SVG
- final arrowLoader = SvgAssetLoader(Assets.arrowDownCircle);
- svg.cache.putIfAbsent(
- arrowLoader.cacheKey(null),
- () => arrowLoader.loadBytes(null),
- );
- }
- // 颜色过渡方法
- void _animateToColor({
- required Color bgStartColor,
- required Color bgEndColor,
- required List<Color> borderColors,
- }) {
- // 如果已经是目标颜色,不需要动画
- if (_currentBgStartColor == bgStartColor &&
- _currentBgEndColor == bgEndColor) {
- return;
- }
- final ColorTween bgStartTween = ColorTween(
- begin: _currentBgStartColor,
- end: bgStartColor,
- );
- final ColorTween bgEndTween = ColorTween(
- begin: _currentBgEndColor,
- end: bgEndColor,
- );
- void listener() {
- if (mounted) {
- setState(() {
- _currentBgStartColor = bgStartTween.evaluate(_colorCtl)!;
- _currentBgEndColor = bgEndTween.evaluate(_colorCtl)!;
- _currentBorderColors = borderColors; // 直接设置边框颜色
- });
- }
- }
- _colorCtl.removeListener(listener);
- _colorCtl.addListener(listener);
- _colorCtl.reset();
- _colorCtl.forward().then((_) {
- _colorCtl.removeListener(listener);
- });
- }
- @override
- bool needReverseAll() {
- return false;
- }
- @override
- Widget buildContent(BuildContext context, RefreshStatus? mode) {
- Widget? child;
- if (mode == RefreshStatus.refreshing) {
- // 刷新状态:启动旋转动画
- if (!_rotateCtl.isAnimating) {
- _rotateCtl.repeat();
- }
- // 确保可见
- if (_dismissCtl.value != 1.0) {
- _dismissCtl.value = 1.0;
- }
- // 确保动画值是0(圆形状态),但不强制设置避免闪烁
- // 如果动画还在执行,让它完成
- if (!_animationController!.isAnimating &&
- _animationController!.value != 0.0) {
- _animationController!.value = 0.0;
- }
- // 和 idle 状态一样显示圆形边框,但图标是旋转的 refresh
- const double circleRadius = 15.0;
- const double topMargin = 12.0;
- final double topCircleCenterY = topMargin + circleRadius;
- // 刷新状态强制使用圆形,避免第一帧动画值未归零导致的闪烁
- const double cylinderHeight = 0.0;
- final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
- final double arrowTopPosition =
- bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
- return widget.refresh ??
- FadeTransition(
- opacity: _dismissCtl,
- child: SizedBox(
- height: 60.0,
- child: Stack(
- clipBehavior: Clip.none,
- children: <Widget>[
- // 绘制圆形边框
- CustomPaint(
- painter: _GradientCircleToCylinderPainter(
- listener: _animationController,
- bgStartColor: _currentBgStartColor,
- bgEndColor: _currentBgEndColor,
- borderColors: _currentBorderColors,
- currentOffset: 42.0, // 强制使用完整圆形的 offset
- isDefaultStyle: true, // 刷新状态使用默认样式渐变方向
- forceCircle: true, // 强制显示圆形,避免闪烁
- ),
- child: Container(height: 60.0),
- ),
- // 旋转的刷新图标
- Positioned(
- left: 0,
- right: 0,
- top: arrowTopPosition,
- child: RotationTransition(
- turns: _rotateCtl,
- child: SvgPicture.asset(
- ReactiveTheme.isLightTheme
- ? Assets.refreshCircleDark
- : Assets.refreshCircle,
- width: 20,
- height: 20,
- ),
- ),
- ),
- ],
- ),
- ),
- );
- } else if (mode == RefreshStatus.completed) {
- // 停止旋转动画
- _rotateCtl.stop();
- // 重置 dismiss 控制器,确保可见
- if (_dismissCtl.value != 1.0) {
- _dismissCtl.value = 1.0;
- }
- // 和刷新状态一样显示圆形边框,但颜色变绿,图标变成成功图标
- const double circleRadius = 15.0;
- const double topMargin = 12.0;
- final double topCircleCenterY = topMargin + circleRadius;
- // 成功状态强制使用圆形
- const double cylinderHeight = 0.0;
- final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
- final double arrowTopPosition =
- bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
- return widget.complete ??
- FadeTransition(
- opacity: _dismissCtl,
- child: SizedBox(
- height: 60.0,
- child: Stack(
- clipBehavior: Clip.none,
- children: <Widget>[
- // 绘制圆形边框(绿色渐变)- 直接使用成功状态颜色
- CustomPaint(
- painter: _GradientCircleToCylinderPainter(
- listener: _animationController,
- bgStartColor: _successBgStartColor,
- bgEndColor: _successBgEndColor,
- borderColors: _successBorderColors,
- currentOffset: 42.0, // 强制使用完整圆形的 offset
- forceCircle: true, // 强制显示圆形
- ),
- child: Container(height: 60.0),
- ),
- // 成功图标
- Positioned(
- left: 0,
- right: 0,
- top: arrowTopPosition,
- child: SvgPicture.asset(
- Assets.successCircle,
- width: 20,
- height: 20,
- ),
- ),
- ],
- ),
- ),
- );
- } else if (mode == RefreshStatus.failed) {
- // 停止旋转动画
- _rotateCtl.stop();
- // 重置 dismiss 控制器,确保可见
- if (_dismissCtl.value != 1.0) {
- _dismissCtl.value = 1.0;
- }
- // 和刷新状态一样显示圆形边框,但颜色变红,图标变成失败图标
- const double circleRadius = 15.0;
- const double topMargin = 12.0;
- final double topCircleCenterY = topMargin + circleRadius;
- // 失败状态强制使用圆形
- const double cylinderHeight = 0.0;
- final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
- final double arrowTopPosition =
- bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
- return widget.failed ??
- FadeTransition(
- opacity: _dismissCtl,
- child: SizedBox(
- height: 60.0,
- child: Stack(
- clipBehavior: Clip.none,
- children: <Widget>[
- // 绘制圆形边框(红色渐变)- 直接使用失败状态颜色
- CustomPaint(
- painter: _GradientCircleToCylinderPainter(
- listener: _animationController,
- bgStartColor: _failedBgStartColor,
- bgEndColor: _failedBgEndColor,
- borderColors: _failedBorderColors,
- currentOffset: 42.0, // 强制使用完整圆形的 offset
- forceCircle: true, // 强制显示圆形
- ),
- child: Container(height: 60.0),
- ),
- // 失败图标
- Positioned(
- left: 0,
- right: 0,
- top: arrowTopPosition,
- child: SvgPicture.asset(
- Assets.failedCircle,
- width: 20,
- height: 20,
- ),
- ),
- ],
- ),
- ),
- );
- } else if (mode == RefreshStatus.idle || mode == RefreshStatus.canRefresh) {
- // 停止旋转动画,恢复原始颜色
- _rotateCtl.stop();
- _animateToColor(
- bgStartColor: _defaultBgStartColor,
- bgEndColor: _defaultBgEndColor,
- borderColors: _defaultBorderColors,
- );
- // 重置 dismiss 控制器,确保可见
- if (_dismissCtl.value != 1.0) {
- _dismissCtl.value = 1.0;
- }
- // 计算箭头位置 - 始终在底部圆(30x30)的中心
- const double circleRadius = 15.0;
- const double topMargin = 12.0;
- final double topCircleCenterY = topMargin + circleRadius; // 顶部圆心Y坐标 = 27
- // 圆柱高度
- final double cylinderHeight = _animationController?.value ?? 0.0;
- // 箭头在底部圆的中心:顶部圆心 + 圆柱延伸高度
- // 当cylinderHeight=0时,箭头在顶部圆中心
- // 当cylinderHeight>0时,箭头在底部圆中心(底部圆心 = 顶部圆心 + 圆柱高度)
- final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
- final double arrowTopPosition =
- bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2),让图标居中
- return FadeTransition(
- opacity: _dismissCtl,
- child: SizedBox(
- height: 60.0,
- child: Stack(
- clipBehavior: Clip.none,
- children: <Widget>[
- // 绘制圆形到圆柱形的渐变边框
- CustomPaint(
- painter: _GradientCircleToCylinderPainter(
- listener: _animationController,
- bgStartColor: _currentBgStartColor,
- bgEndColor: _currentBgEndColor,
- borderColors: _currentBorderColors,
- currentOffset: _currentOffset,
- isDefaultStyle: true, // idle/canRefresh 状态使用默认样式渐变方向
- ),
- child: Container(height: 60.0),
- ),
- // 箭头在底部圆(30x30)的中心
- Positioned(
- left: 0,
- right: 0,
- top: arrowTopPosition,
- child: SvgPicture.asset(
- ReactiveTheme.isLightTheme
- ? Assets.arrowDownCircleDark
- : Assets.arrowDownCircle,
- width: 20,
- height: 20,
- ),
- ),
- ],
- ),
- ),
- );
- }
- return SizedBox(height: 60.0, child: Center(child: child));
- }
- @override
- void resetValue() {
- _animationController!.reset();
- _dismissCtl.value = 1.0;
- _currentOffset = 0.0;
- _rotateCtl.reset();
- _colorCtl.reset();
- // 重置为原始颜色
- _currentBgStartColor = _defaultBgStartColor;
- _currentBgEndColor = _defaultBgEndColor;
- _currentBorderColors = List.from(_defaultBorderColors);
- }
- @override
- void dispose() {
- _dismissCtl.dispose();
- _animationController!.dispose();
- _rotateCtl.dispose();
- _colorCtl.dispose();
- super.dispose();
- }
- }
- /// 绘制从圆形到圆柱形的渐变边框效果
- class _GradientCircleToCylinderPainter extends CustomPainter {
- final Animation<double>? listener;
- final Color bgStartColor;
- final Color bgEndColor;
- final List<Color> borderColors;
- final double currentOffset; // 当前下拉距离
- final bool isDefaultStyle; // 是否为默认/刷新状态的样式
- final bool forceCircle; // 强制显示圆形(用于 refreshing/completed/failed 状态)
- double get value => forceCircle ? 0.0 : listener!.value;
- _GradientCircleToCylinderPainter({
- this.listener,
- required this.bgStartColor,
- required this.bgEndColor,
- required this.borderColors,
- required this.currentOffset,
- this.isDefaultStyle = false,
- this.forceCircle = false,
- }) : super(repaint: forceCircle ? null : listener); // forceCircle 时不需要监听动画
- @override
- void paint(Canvas canvas, Size size) {
- final double middleW = size.width / 2;
- final double circleRadius = 15.0; // 圆的半径(改为15)
- final double strokeWidth = 2.0; // 边框宽度
- final double topMargin = 12.0; // 顶部边距
- const double circleFullyVisibleOffset = 42.0; // 圆形完全显示需要的距离(12 + 30 = 42)
- // 圆心位置
- final double circleCenterY = topMargin + circleRadius;
- // 计算下拉的距离,控制圆柱的高度
- final double pullDistance = value;
- // 背景渐变色
- final Gradient backgroundGradient = LinearGradient(
- colors: [bgStartColor, bgEndColor],
- // 默认/刷新状态:从右到左,其他状态:从上到下
- begin: isDefaultStyle ? Alignment.centerRight : Alignment.topCenter,
- end: isDefaultStyle ? Alignment.centerLeft : Alignment.bottomCenter,
- );
- // 边框渐变色(支持多个颜色)
- final Gradient borderGradient = LinearGradient(
- colors: borderColors,
- // 默认/刷新状态:从下到上(反转),其他状态:从上到下
- begin: isDefaultStyle ? Alignment.bottomCenter : Alignment.topCenter,
- end: isDefaultStyle ? Alignment.topCenter : Alignment.bottomCenter,
- );
- // 阴影效果
- final Paint shadowPaint = Paint()
- ..color = Colors.black.withOpacity(0.1)
- ..maskFilter = MaskFilter.blur(BlurStyle.normal, 2);
- // 第一阶段:圆形逐渐显示(使用裁剪)
- if (currentOffset < circleFullyVisibleOffset) {
- canvas.save();
- // 裁剪显示区域,让圆形从顶部逐渐出现
- canvas.clipRect(Rect.fromLTWH(0, 0, size.width, currentOffset));
- // 绘制阴影
- canvas.drawCircle(
- Offset(middleW, circleCenterY + 1),
- circleRadius,
- shadowPaint,
- );
- // 绘制背景
- final Rect bgRect = Rect.fromCircle(
- center: Offset(middleW, circleCenterY),
- radius: circleRadius,
- );
- final Paint bgPaint = Paint()
- ..shader = backgroundGradient.createShader(bgRect)
- ..style = PaintingStyle.fill;
- canvas.drawCircle(Offset(middleW, circleCenterY), circleRadius, bgPaint);
- // 绘制边框(渐变)
- final Paint borderPaint = Paint()
- ..shader = borderGradient.createShader(bgRect)
- ..style = PaintingStyle.stroke
- ..strokeWidth = strokeWidth
- ..strokeCap = StrokeCap.round;
- canvas.drawCircle(
- Offset(middleW, circleCenterY),
- circleRadius,
- borderPaint,
- );
- canvas.restore();
- } else {
- // 第二阶段:绘制圆柱形效果(包括 pullDistance = 0 的情况)
- if (pullDistance <= 0.1) {
- // pullDistance很小时,只绘制圆形
- // 绘制阴影
- canvas.drawCircle(
- Offset(middleW, circleCenterY + 1),
- circleRadius,
- shadowPaint,
- );
- // 绘制背景
- final Rect bgRect = Rect.fromCircle(
- center: Offset(middleW, circleCenterY),
- radius: circleRadius,
- );
- final Paint bgPaint = Paint()
- ..shader = backgroundGradient.createShader(bgRect)
- ..style = PaintingStyle.fill;
- canvas.drawCircle(
- Offset(middleW, circleCenterY),
- circleRadius,
- bgPaint,
- );
- // 绘制边框(渐变)
- final Paint borderPaint = Paint()
- ..shader = borderGradient.createShader(bgRect)
- ..style = PaintingStyle.stroke
- ..strokeWidth = strokeWidth
- ..strokeCap = StrokeCap.round;
- canvas.drawCircle(
- Offset(middleW, circleCenterY),
- circleRadius,
- borderPaint,
- );
- } else {
- // 绘制圆柱
- Path path = Path();
- // 绘制顶部半圆(上半部分)
- path.addArc(
- Rect.fromCircle(
- center: Offset(middleW, circleCenterY),
- radius: circleRadius,
- ),
- -3.14159, // π (左侧)
- 3.14159, // π (到右侧)
- );
- // 右侧垂直线
- path.lineTo(middleW + circleRadius, circleCenterY + pullDistance);
- // 绘制底部半圆
- path.addArc(
- Rect.fromCircle(
- center: Offset(middleW, circleCenterY + pullDistance),
- radius: circleRadius,
- ),
- 0, // 0 (右侧)
- 3.14159, // π (到左侧)
- );
- // 左侧垂直线(回到起点)
- path.lineTo(middleW - circleRadius, circleCenterY);
- // 绘制阴影
- canvas.drawPath(path.shift(Offset(0, 1)), shadowPaint);
- // 绘制背景
- final Rect bgRect = Rect.fromLTWH(
- middleW - circleRadius,
- circleCenterY - circleRadius,
- circleRadius * 2,
- circleRadius * 2 + pullDistance,
- );
- final Paint bgPaint = Paint()
- ..shader = backgroundGradient.createShader(bgRect)
- ..style = PaintingStyle.fill;
- canvas.drawPath(path, bgPaint);
- // 绘制边框(渐变)
- final Paint borderPaint = Paint()
- ..shader = borderGradient.createShader(bgRect)
- ..style = PaintingStyle.stroke
- ..strokeWidth = strokeWidth
- ..strokeCap = StrokeCap.round;
- canvas.drawPath(path, borderPaint);
- }
- }
- }
- @override
- bool shouldRepaint(CustomPainter oldDelegate) {
- return true;
- }
- }
|