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

求助:开发Flutter自定义渐变边框环形计时器组件

Flutter 自定义环形倒计时计时器组件实现方案

直接通过CustomPainter实现完全自定义的计时器,满足你需要的渐变环形、端点标记和中心数值显示需求,代码及说明如下:

完整实现代码

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

class CircularCountdownTimer extends StatefulWidget {
  final int duration; // 倒计时总时长(秒)
  final double strokeWidth; // 环形边框宽度
  final Color startColor; // 渐变起始色
  final Color endColor; // 渐变结束色
  final double indicatorRadius; // 端点标记圆半径
  final TextStyle textStyle; // 中心文本样式

  const CircularCountdownTimer({
    super.key,
    required this.duration,
    this.strokeWidth = 8.0,
    this.startColor = Colors.blue,
    this.endColor = Colors.purple,
    this.indicatorRadius = 6.0,
    this.textStyle = const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
  });

  @override
  State<CircularCountdownTimer> createState() => _CircularCountdownTimerState();
}

class _CircularCountdownTimerState extends State<CircularCountdownTimer> with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _progressAnimation;
  int _remainingSeconds = 0;

  @override
  void initState() {
    super.initState();
    _remainingSeconds = widget.duration;
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: widget.duration),
    );
    _progressAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(_controller)
      ..addListener(() {
        setState(() {
          _remainingSeconds = (widget.duration * _progressAnimation.value).round();
        });
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          // 可在此添加倒计时结束后的自定义逻辑
        }
      });
    _controller.forward();
  }

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

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 200,
      height: 200,
      child: Stack(
        alignment: Alignment.center,
        children: [
          CustomPaint(
            painter: _CircularTimerPainter(
              progress: _progressAnimation.value,
              strokeWidth: widget.strokeWidth,
              startColor: widget.startColor,
              endColor: widget.endColor,
              indicatorRadius: widget.indicatorRadius,
            ),
            size: const Size(200, 200),
          ),
          Text(
            '$_remainingSeconds',
            style: widget.textStyle,
          ),
        ],
      ),
    );
  }
}

class _CircularTimerPainter extends CustomPainter {
  final double progress;
  final double strokeWidth;
  final Color startColor;
  final Color endColor;
  final double indicatorRadius;

  _CircularTimerPainter({
    required this.progress,
    required this.strokeWidth,
    required this.startColor,
    required this.endColor,
    required this.indicatorRadius,
  });

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

    // 绘制背景环形(可选,用于区分已完成/未完成部分)
    final backgroundPaint = Paint()
      ..color = Colors.grey.shade200
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;
    canvas.drawCircle(center, radius, backgroundPaint);

    // 渐变环形画笔
    final gradient = SweepGradient(
      startAngle: -pi / 2,
      endAngle: 3 * pi / 2,
      colors: [startColor, endColor],
    );
    final progressPaint = Paint()
      ..shader = gradient.createShader(Rect.fromCircle(center: center, radius: radius))
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.round;

    // 绘制随进度收缩的环形
    final sweepAngle = 2 * pi * progress;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      -pi / 2, // 起始角度(顶部)
      sweepAngle,
      false,
      progressPaint,
    );

    // 绘制端点圆形标记
    if (progress > 0) {
      final indicatorAngle = -pi / 2 + sweepAngle;
      final indicatorOffset = Offset(
        center.dx + radius * cos(indicatorAngle),
        center.dy + radius * sin(indicatorAngle),
      );
      final indicatorPaint = Paint()
        ..color = endColor
        ..style = PaintingStyle.fill;
      canvas.drawCircle(indicatorOffset, indicatorRadius, indicatorPaint);
    }
  }

  @override
  bool shouldRepaint(covariant _CircularTimerPainter oldDelegate) {
    return progress != oldDelegate.progress ||
        strokeWidth != oldDelegate.strokeWidth ||
        startColor != oldDelegate.startColor ||
        endColor != oldDelegate.endColor ||
        indicatorRadius != oldDelegate.indicatorRadius;
  }
}

使用示例

在页面中直接调用组件:

CircularCountdownTimer(
  duration: 60,
  strokeWidth: 10,
  startColor: Colors.orange,
  endColor: Colors.red,
  indicatorRadius: 7,
  textStyle: const TextStyle(
    fontSize: 36,
    fontWeight: FontWeight.bold,
    color: Colors.black87,
  ),
)

核心实现说明

  • 渐变环形:通过SweepGradient创建环形渐变,解决了原生CircularProgressIndicator无法添加渐变的问题。
  • 端点标记:根据当前进度计算标记的坐标,实时跟随环形收缩端点移动。
  • 进度控制:用AnimationController驱动进度动画,同步更新剩余秒数和环形绘制状态。
  • 可配置化:将时长、边框宽度、颜色、标记大小等参数封装为组件属性,方便复用和自定义。

扩展方向

  • 添加暂停/继续功能:通过_controller.stop()和_controller.forward()控制动画状态。
  • 增加结束回调:给组件添加VoidCallback参数,用于倒计时结束后的逻辑处理。
  • 自定义起始角度:修改drawArc的起始角度参数,实现从任意位置开始的倒计时。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 20:33:29