|
@@ -0,0 +1,679 @@
|
|
|
|
|
+import 'dart:async';
|
|
|
|
|
+import 'package:flutter/material.dart'
|
|
|
|
|
+ hide RefreshIndicatorState, RefreshIndicator;
|
|
|
|
|
+import 'package:flutter_svg/flutter_svg.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;
|
|
|
|
|
+
|
|
|
|
|
+ /// idle Icon center in circle
|
|
|
|
|
+ final Widget idleIcon;
|
|
|
|
|
+
|
|
|
|
|
+ GradientCircleHeader({
|
|
|
|
|
+ super.key,
|
|
|
|
|
+ this.refresh,
|
|
|
|
|
+ this.complete,
|
|
|
|
|
+ super.completeDuration = const Duration(milliseconds: 600),
|
|
|
|
|
+ this.failed,
|
|
|
|
|
+ Widget? idleIcon,
|
|
|
|
|
+ }) : idleIcon =
|
|
|
|
|
+ idleIcon ??
|
|
|
|
|
+ SvgPicture.asset(Assets.arrowDownCircle, width: 20, height: 20),
|
|
|
|
|
+ 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; // 跟踪当前下拉距离
|
|
|
|
|
+
|
|
|
|
|
+ // 默认状态的背景渐变
|
|
|
|
|
+ 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();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 颜色过渡方法
|
|
|
|
|
+ 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;
|
|
|
|
|
+ final double cylinderHeight = _animationController?.value ?? 0.0;
|
|
|
|
|
+ final double bottomCircleCenterY = topCircleCenterY + cylinderHeight;
|
|
|
|
|
+ final double arrowTopPosition =
|
|
|
|
|
+ bottomCircleCenterY - 10.0; // 10是图标高度的一半(20/2)
|
|
|
|
|
+
|
|
|
|
|
+ return widget.refresh ??
|
|
|
|
|
+ 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, // 刷新状态使用默认样式渐变方向
|
|
|
|
|
+ ),
|
|
|
|
|
+ child: Container(height: 60.0),
|
|
|
|
|
+ ),
|
|
|
|
|
+ // 旋转的刷新图标
|
|
|
|
|
+ Positioned(
|
|
|
|
|
+ left: 0,
|
|
|
|
|
+ right: 0,
|
|
|
|
|
+ top: arrowTopPosition,
|
|
|
|
|
+ child: RotationTransition(
|
|
|
|
|
+ turns: _rotateCtl,
|
|
|
|
|
+ child: SvgPicture.asset(
|
|
|
|
|
+ Assets.refreshCircle,
|
|
|
|
|
+ width: 20,
|
|
|
|
|
+ height: 20,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ } else if (mode == RefreshStatus.completed) {
|
|
|
|
|
+ // 停止旋转动画,启动颜色过渡动画
|
|
|
|
|
+ _rotateCtl.stop();
|
|
|
|
|
+ _animateToColor(
|
|
|
|
|
+ bgStartColor: _successBgStartColor,
|
|
|
|
|
+ bgEndColor: _successBgEndColor,
|
|
|
|
|
+ borderColors: _successBorderColors,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 重置 dismiss 控制器,确保可见
|
|
|
|
|
+ if (_dismissCtl.value != 1.0) {
|
|
|
|
|
+ _dismissCtl.value = 1.0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保动画值是0(圆形状态)
|
|
|
|
|
+ if (!_animationController!.isAnimating &&
|
|
|
|
|
+ _animationController!.value != 0.0) {
|
|
|
|
|
+ _animationController!.value = 0.0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 和刷新状态一样显示圆形边框,但颜色变绿,图标变成成功图标
|
|
|
|
|
+ const double circleRadius = 15.0;
|
|
|
|
|
+ const double topMargin = 12.0;
|
|
|
|
|
+ final double topCircleCenterY = topMargin + circleRadius;
|
|
|
|
|
+ final double cylinderHeight = _animationController?.value ?? 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: _currentBgStartColor,
|
|
|
|
|
+ bgEndColor: _currentBgEndColor,
|
|
|
|
|
+ borderColors: _currentBorderColors,
|
|
|
|
|
+ currentOffset: _currentOffset,
|
|
|
|
|
+ ),
|
|
|
|
|
+ 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();
|
|
|
|
|
+ _animateToColor(
|
|
|
|
|
+ bgStartColor: _failedBgStartColor,
|
|
|
|
|
+ bgEndColor: _failedBgEndColor,
|
|
|
|
|
+ borderColors: _failedBorderColors,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 重置 dismiss 控制器,确保可见
|
|
|
|
|
+ if (_dismissCtl.value != 1.0) {
|
|
|
|
|
+ _dismissCtl.value = 1.0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保动画值是0(圆形状态)
|
|
|
|
|
+ if (!_animationController!.isAnimating &&
|
|
|
|
|
+ _animationController!.value != 0.0) {
|
|
|
|
|
+ _animationController!.value = 0.0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 和刷新状态一样显示圆形边框,但颜色变红,图标变成失败图标
|
|
|
|
|
+ const double circleRadius = 15.0;
|
|
|
|
|
+ const double topMargin = 12.0;
|
|
|
|
|
+ final double topCircleCenterY = topMargin + circleRadius;
|
|
|
|
|
+ final double cylinderHeight = _animationController?.value ?? 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: _currentBgStartColor,
|
|
|
|
|
+ bgEndColor: _currentBgEndColor,
|
|
|
|
|
+ borderColors: _currentBorderColors,
|
|
|
|
|
+ currentOffset: _currentOffset,
|
|
|
|
|
+ ),
|
|
|
|
|
+ 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: widget.idleIcon,
|
|
|
|
|
+ ),
|
|
|
|
|
+ ],
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ 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; // 是否为默认/刷新状态的样式
|
|
|
|
|
+
|
|
|
|
|
+ double get value => listener!.value;
|
|
|
|
|
+
|
|
|
|
|
+ _GradientCircleToCylinderPainter({
|
|
|
|
|
+ this.listener,
|
|
|
|
|
+ required this.bgStartColor,
|
|
|
|
|
+ required this.bgEndColor,
|
|
|
|
|
+ required this.borderColors,
|
|
|
|
|
+ required this.currentOffset,
|
|
|
|
|
+ this.isDefaultStyle = false,
|
|
|
|
|
+ }) : super(repaint: listener);
|
|
|
|
|
+
|
|
|
|
|
+ @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;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|