You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现先渐变动画后可拖动旋转的圆形内小球

问题与解决方案:渐变动画完成后旋转白色小球

问题描述

如何在渐变动画绘制完成后,旋转圆形内部的白色小球?当前为使首个渐变动画生效,需将tween传入animation;而实现旋转则需为控制器添加监听器,请问如何结合这两种实现方式?我需要先触发渐变动画,之后能够旋转小球,已使用GestureDetector,但不知如何正确组合这两个动画。此外,小球初始位置需基于DateTime.now(),不知如何正确实现小球旋转。

解决方案

1. 监听渐变动画完成事件,切换控制模式

给渐变动画控制器添加状态监听器,动画完成后标记切换为可旋转模式,同时根据当前时间初始化旋转控制器的初始值。

2. 基于DateTime.now()设置小球初始位置

根据当前时间的小时+分钟比例计算初始角度,让小球一开始就对应正确的时钟位置。

3. 修正手势控制逻辑

仅在渐变动画完成后响应手势拖动,避免干扰渐变动画的执行过程。

4. 调整CustomPainter的角度计算

区分渐变阶段和旋转阶段的角度计算逻辑,渐变阶段使用动画进度,旋转阶段使用旋转控制器的进度。


修改后的_ContentState类

import 'dart:math' as math;
import 'package:flutter/material.dart';

class _Content extends StatefulWidget {
  @override
  _ContentState createState() => _ContentState();
}

class _ContentState extends State<_Content> with TickerProviderStateMixin {
  late AnimationController _controller;
  late AnimationController _rotateController;
  late Animation<double> _animation;
  late final Animation<double> _fadeAnimation;
  final Duration duration = const Duration(seconds: 5);
  bool _gradientAnimationCompleted = false;
  DateTime _startTime = DateTime.now();

  @override
  void initState() {
    super.initState();
    _startTime = DateTime.now();
    
    // 渐变动画控制器
    _controller = AnimationController(
      vsync: this,
      duration: duration,
    )..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          setState(() {
            _gradientAnimationCompleted = true;
          });
          // 根据当前时间初始化旋转控制器初始值
          final double startHour = _startTime.hour.toDouble() % 12;
          final double initialProgress = (startHour + _startTime.minute / 60) / 12;
          _rotateController.value = initialProgress;
        }
      })
      ..forward();

    // 旋转控制器
    _rotateController = AnimationController(
      duration: const Duration(seconds: 5),
      vsync: this,
    );

    _fadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.easeIn,
      ),
    );
    _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
  }

  void _onPanUpdate(DragUpdateDetails details, Size size) {
    if (!_gradientAnimationCompleted) return; // 渐变未完成时不响应手势

    final Offset center = Offset(
      size.width / 2,
      size.height / 2,
    );
    final Offset position = details.localPosition - center;
    final double angle = math.atan2(position.dy, position.dx);
    // 将角度转换为0-1范围的进度值
    final double progress = (angle + math.pi / 2) / (2 * math.pi);
    setState(() {
      _rotateController.value = progress.clamp(0.0, 1.0);
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    _rotateController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Padding(
          padding: const EdgeInsets.only(right: 20, top: 26),
          child: Align(
            alignment: Alignment.topRight,
            child: IconButton(
              onPressed: () {},
              icon: const Icon(Icons.close),
            ),
          ),
        ),
        GestureDetector(
          onPanUpdate: (DragUpdateDetails details) => _onPanUpdate(details, const Size(340, 340)),
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                AnimatedBuilder(
                  animation: Listenable.merge([_animation, _rotateController]),
                  builder: (BuildContext context, Widget? child) {
                    return SizedBox(
                      width: 340,
                      height: 340,
                      child: CustomPaint(
                        painter: TimerPainter(
                          progress: _animation.value,
                          context: context,
                          startTime: _startTime,
                          rotationProgress: _rotateController.value,
                          isGradientCompleted: _gradientAnimationCompleted,
                        ),
                        child: Center(
                          child: AnimatedBuilder(
                            animation: _fadeAnimation,
                            builder: (BuildContext context, Widget? child) {
                              return Opacity(
                                opacity: _fadeAnimation.value,
                                child: Text(
                                  _startTime.toString(),
                                  style: const TextStyle(fontSize: 64, color: Colors.white),
                                ),
                              );
                            },
                          ),
                        ),
                      ),
                    );
                  },
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }
}

修改后的TimerPainter类

import 'dart:math';
import 'package:flutter/material.dart';

class TimerPainter extends CustomPainter {
  TimerPainter({
    required this.progress,
    required this.context,
    required this.startTime,
    required this.rotationProgress,
    required this.isGradientCompleted,
  });
  final double progress;
  final double rotationProgress;
  final BuildContext context;
  final DateTime startTime;
  final bool isGradientCompleted;

  @override
  void paint(Canvas canvas, Size size) {
    const double strokeWidth = 48;
    final double radius = (size.shortestSide - strokeWidth) / 2;
    final center = Offset(size.width / 2, size.height / 2);

    // 绘制黑色外圈
    final Paint outerCircle = Paint()
      ..strokeWidth = strokeWidth
      ..color = Colors.black
      ..style = PaintingStyle.stroke;
    canvas.drawCircle(center, radius, outerCircle);

    // 计算初始角度(基于当前时间)
    final double startHour = startTime.hour.toDouble() % 12;
    final double startMinute = startTime.minute.toDouble();
    final double startAngle = ((startHour + startMinute / 60) / 12) * 2 * pi - pi / 2;

    double currentAngle;
    if (isGradientCompleted) {
      // 旋转阶段:使用旋转控制器的进度计算角度
      currentAngle = startAngle + 2 * pi * rotationProgress;
    } else {
      // 渐变阶段:使用渐变动画进度计算角度
      currentAngle = startAngle + 2 * pi * progress;
    }

    // 绘制渐变圆弧
    final Paint gradientCircle = Paint()
      ..strokeWidth = strokeWidth
      ..shader = SweepGradient(
        tileMode: TileMode.repeated,
        transform: GradientRotation(currentAngle),
        colors: const [
          Colors.red,
          Colors.blue,
          Colors.yellow
        ],
      ).createShader(
        Rect.fromCircle(center: center, radius: radius),
      )
      ..style = PaintingStyle.stroke;

    final drawAngle = isGradientCompleted ? 2 * pi : 2 * pi * progress;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      startAngle,
      drawAngle,
      false,
      gradientCircle,
    );

    // 绘制白色小球
    final Paint spotPaint = Paint()
      ..color = Colors.white
      ..style = PaintingStyle.fill;

    final double spotX = center.dx + radius * cos(currentAngle);
    final double spotY = center.dy + radius * sin(currentAngle);
    canvas.drawCircle(Offset(spotX, spotY), strokeWidth / 2, spotPaint);
  }

  @override
  bool shouldRepaint(covariant TimerPainter oldDelegate) {
    return oldDelegate.progress != progress ||
        oldDelegate.rotationProgress != rotationProgress ||
        oldDelegate.isGradientCompleted != isGradientCompleted;
  }
}

内容的提问来源于stack exchange,提问作者user16357470

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 14:28:09