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
相关产品推荐
相关产品推荐

