如何在Flutter中实现类似cariocamenu的滚动圆形框轨迹动画?
在Flutter中实现滚动跟随的圆形轨迹指示器动画
完全可以实现这种效果,核心是通过滚动监听获取位置,结合自定义绘制和动画过渡来完成,以下是具体实现方案:
核心思路
- 监听滚动位置:用
ScrollController捕获列表的滚动偏移,计算出当前需要高亮的列表项位置。 - 平滑动画过渡:通过
AnimationController和Tween实现指示器在不同位置间的平滑移动,避免生硬跳转。 - 自定义绘制轨迹:使用
CustomPainter绘制垂直轨迹线、圆形指示器,以及可选的轨迹节点。
完整代码实现
import 'package:flutter/material.dart'; class CircularTrailIndicator extends StatefulWidget { const CircularTrailIndicator({super.key}); @override State<CircularTrailIndicator> createState() => _CircularTrailIndicatorState(); } class _CircularTrailIndicatorState extends State<CircularTrailIndicator> with SingleTickerProviderStateMixin { final ScrollController _scrollController = ScrollController(); late AnimationController _animationController; late Animation<double> _positionAnimation; double _targetY = 0; // 可自定义的参数 final double _itemHeight = 60; final double _indicatorRadius = 12; final double _trailWidth = 2; final int _listItemCount = 10; @override void initState() { super.initState(); // 初始化动画控制器 _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _positionAnimation = Tween<double>(begin: 0, end: 0).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); // 监听滚动事件,更新指示器目标位置 _scrollController.addListener(() { final scrollOffset = _scrollController.offset; // 计算当前对应的列表项索引,限制在合法范围内 final targetIndex = (scrollOffset / _itemHeight).round().clamp(0, _listItemCount - 1); // 计算指示器目标Y坐标(列表项中心位置) setState(() { _targetY = targetIndex * _itemHeight + _itemHeight / 2; }); // 启动动画,从当前位置过渡到目标位置 _animationController.forward(from: 0); }); } @override void dispose() { _scrollController.dispose(); _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: Row( children: [ // 左侧轨迹与指示器区域 AnimatedBuilder( animation: _positionAnimation, builder: (context, child) { // 插值计算当前指示器的Y坐标 final currentY = lerpDouble(_positionAnimation.value, _positionAnimation.value, _targetY)!; return CustomPaint( size: const Size(40, double.infinity), painter: _TrailPainter( currentY: currentY, indicatorRadius: _indicatorRadius, trailWidth: _trailWidth, itemCount: _listItemCount, itemHeight: _itemHeight, ), ); }, ), // 右侧滚动列表 Expanded( child: ListView.builder( controller: _scrollController, itemCount: _listItemCount, itemBuilder: (context, index) { return SizedBox( height: _itemHeight, child: Center(child: Text('列表项 $index')), ); }, ), ), ], ), ); } } // 自定义画笔,负责绘制轨迹和指示器 class _TrailPainter extends CustomPainter { final double currentY; final double indicatorRadius; final double trailWidth; final int itemCount; final double itemHeight; _TrailPainter({ required this.currentY, required this.indicatorRadius, required this.trailWidth, required this.itemCount, required this.itemHeight, }); @override void paint(Canvas canvas, Size size) { final centerX = size.width / 2; // 绘制主轨迹线 final trailPaint = Paint() ..color = Colors.blue ..strokeWidth = trailWidth ..style = PaintingStyle.stroke; canvas.drawLine( Offset(centerX, indicatorRadius), Offset(centerX, size.height - indicatorRadius), trailPaint, ); // 绘制圆形指示器 final indicatorPaint = Paint()..color = Colors.blue; canvas.drawCircle( Offset(centerX, currentY), indicatorRadius, indicatorPaint, ); // 绘制轨迹上的白色节点(可选,增强视觉层次) final nodePaint = Paint() ..color = Colors.white ..strokeWidth = 2; for (int i = 0; i < itemCount; i++) { final nodeY = i * itemHeight + itemHeight / 2; canvas.drawCircle(Offset(centerX, nodeY), 4, nodePaint); } } @override bool shouldRepaint(covariant _TrailPainter oldDelegate) { // 仅当指示器位置变化时重绘 return oldDelegate.currentY != currentY; } }
自定义调整点
- 修改
_itemHeight适配你的列表项高度; - 调整
_indicatorRadius、_trailWidth改变指示器和轨迹的尺寸; - 替换
Colors.blue为你需要的主题色; - 若列表项高度不固定,可以通过
RenderObject获取每个列表项的实际位置,替代固定高度计算。
内容的提问来源于stack exchange,提问作者Ash Khachatryan
相关产品推荐
相关产品推荐

