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

Flutter如何实现支持独立动画的多色填充半圆组件

Flutter 独立可动画分块半圆实现方案

核心逻辑是放弃整容器渐变、一次性CustomPaint绘制全半圆的思路,把每个颜色扇形拆成独立可管理的StatefulWidget,单独挂载动画控制器,最后通过Stack拼接成完整半圆,每个区块的动画完全互不干扰。

具体实现步骤

  • 拆分区块参数
    半圆总弧度为180°,根据你需要的色块数量,按比例给每个色块分配独立的起始角度、扫过角度参数,每个色块单独封装为Widget,内部管理自己的动画逻辑(点击反馈、呼吸效果、选中高亮都可以单独实现)。
  • 封装单块扇形组件
    单块扇形用ClipPath裁剪出对应角度的形状,内部可以填充纯色、渐变甚至嵌套任意子组件,配合Transform旋转到对应角度位置。核心实现代码如下(使用前需导入dart:math库):
// 独立扇形色块组件,每个实例自带独立动画控制器
class SectorBlock extends StatefulWidget {
  final Color fillColor;
  final double startAngle; // 色块起始角度,0°对应半圆最右端点
  final double sweepAngle; // 色块自身占据的角度
  final double radius; // 整圆半径
  const SectorBlock({
    super.key,
    required this.fillColor,
    required this.startAngle,
    required this.sweepAngle,
    required this.radius,
  });

  @override
  State<SectorBlock> createState() => _SectorBlockState();
}

class _SectorBlockState extends State<SectorBlock> with SingleTickerProviderStateMixin {
  late final AnimationController _blockAnimCtrl;
  late final Animation<double> _scaleAnim;

  @override
  void initState() {
    super.initState();
    // 每个色块自己的动画控制器,不依赖父组件
    _blockAnimCtrl = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 280),
    );
    _scaleAnim = Tween(begin: 1.0, end: 1.1).animate(
      CurvedAnimation(parent: _blockAnimCtrl, curve: Curves.easeOutBack)
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      // 示例:点击触发放大动画,可替换成任意你需要的动画逻辑
      onTapDown: (_) => _blockAnimCtrl.forward(),
      onTapUp: (_) => _blockAnimCtrl.reverse(),
      onTapCancel: () => _blockAnimCtrl.reverse(),
      child: Transform.rotate(
        // 旋转到对应起始角度
        angle: widget.startAngle * pi / 180,
        alignment: Alignment.bottomLeft, // 所有色块统一对齐到圆心点
        child: AnimatedBuilder(
          animation: _scaleAnim,
          builder: (context, child) {
            return Transform.scale(
              scale: _scaleAnim.value,
              alignment: Alignment.bottomLeft,
              child: ClipPath(
                clipper: SectorClipper(
                  sweepAngle: widget.sweepAngle,
                  radius: widget.radius,
                ),
                child: Container(
                  width: widget.radius,
                  height: widget.radius,
                  color: widget.fillColor,
                ),
              ),
            );
          }
        ),
      ),
    );
  }
}

// 扇形路径裁剪器
class SectorClipper extends CustomClipper<Path> {
  final double sweepAngle;
  final double radius;
  SectorClipper({required this.sweepAngle, required this.radius});

  @override
  Path getClip(Size size) {
    final Path path = Path();
    final double sweepRad = sweepAngle * pi / 180;
    path.moveTo(0, size.height); // 定位到圆心点
    path.lineTo(size.width, size.height); // 绘制0°方向的半径边
    path.arcTo(
      Rect.fromCircle(center: Offset(0, size.height), radius: radius),
      0,
      -sweepRad, // 上半圆向上扫用负角度,做下半圆去掉负号即可
      false
    );
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant SectorClipper oldClipper) {
    return oldClipper.sweepAngle != sweepAngle || oldClipper.radius != radius;
  }
}
  • 拼接完整半圆
    用Stack作为父容器,设置容器尺寸为宽=2*radius、高=radius(上半圆尺寸),把所有SectorBlock组件按计算好的起始角度依次放入Stack,所有组件的圆心对齐到Stack的左下角位置,最后在Stack底部加上直径边的装饰条即可还原示例效果。

避坑提示:所有色块的旋转、缩放对齐点必须完全统一,不然拼接处会出现错位、缝隙。如果需要色块之间留留白间隙,只需要给每个色块的sweepAngle减去1~2°,对应调整起始角度的偏移量就行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 15:09:21