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 createState() { return _GradientCircleHeaderState(); } } class _GradientCircleHeaderState extends RefreshIndicatorState 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 _defaultBorderColors = const [ Color(0xFFDFDFDF), Color(0xFFA2A2A2), Color(0x4F6B6B6B), ]; // 当前状态的背景渐变颜色 Color _currentBgStartColor = const Color(0xA6BABABA); Color _currentBgEndColor = const Color(0x33404040); // 当前状态的边框渐变颜色 List _currentBorderColors = [ const Color(0xFFDFDFDF), const Color(0xFFA2A2A2), const Color(0x4F6B6B6B), ]; // 成功状态的背景渐变 final Color _successBgStartColor = const Color(0xFF0D2116); final Color _successBgEndColor = const Color(0xFF228643); // 成功状态的边框渐变 final List _successBorderColors = [ const Color(0xFF253B2E), const Color(0xFF41905A), const Color(0xFF7BF4AB), ]; // 失败状态的背景渐变 final Color _failedBgStartColor = const Color(0xFF1E1215); final Color _failedBgEndColor = const Color(0xFFAA4140); // 失败状态的边框渐变 final List _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 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 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: [ // 绘制圆形边框 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: [ // 绘制圆形边框(绿色渐变)- 直接使用成功状态颜色 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: [ // 绘制圆形边框(红色渐变)- 直接使用失败状态颜色 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: [ // 绘制圆形到圆柱形的渐变边框 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? listener; final Color bgStartColor; final Color bgEndColor; final List 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; } }