如何获取动画曲线上点的近似时间,同步按钮颜色变化?
Flutter进度条动画与选项颜色同步解决方案
针对非曲线下进度条与选项颜色不同步的问题,以下是两种精准同步的实现方案:
方案一:通过曲线逆运算计算精准延迟时间
Flutter的Curve类通过transform(t)方法将0-1的时间进度t映射为0-1的动画进度值。要实现颜色变化与进度条经过时机同步,需要反向计算:当动画进度达到选项位置时,对应的时间t是多少,再以此设置Future.delayed的延迟时长。
由于多数曲线没有解析逆函数,可用二分法近似求解对应时间值:
// 计算动画进度达到targetProgress时对应的时间比例t(0-1) double findTimeForProgress(Curve curve, double targetProgress, {int iterations = 10}) { double low = 0.0; double high = 1.0; // 迭代10次足以满足精度要求 for (int i = 0; i < iterations; i++) { double mid = (low + high) / 2; double currentProgress = curve.transform(mid); if (currentProgress < targetProgress) { low = mid; } else { high = mid; } } return (low + high) / 2; }
修改alteraCorItemStepper函数,替换原有均分时间逻辑:
alteraCorItemStepper(int indiceItem){ const totalDurationMs = 2000; const curve = Curves.slowMiddle; int totalItems = listaProdutos.length; if(ultimoIndicePressionado < indiceItem){ // 目标选项的进度阈值(适配spaceBetween布局) double targetProgress = indiceItem / (totalItems - 1); double targetTimeRatio = findTimeForProgress(curve, targetProgress); int targetDelay = (totalDurationMs * targetTimeRatio).round(); Future.delayed(Duration(milliseconds: targetDelay), () { listaProdutos[indiceItem].corWidget = res.azul_claro; listaProdutos.refresh(); }); for(int i = 1; i <= indiceItem - ultimoIndicePressionado; i++){ int indexAlterar = ultimoIndicePressionado + i; double itemProgress = indexAlterar / (totalItems - 1); double timeRatio = findTimeForProgress(curve, itemProgress); int delay = (totalDurationMs * timeRatio).round(); Future.delayed(Duration(milliseconds: delay), () { if(indexAlterar <= indiceItem){ listaProdutos[indexAlterar].corWidget = res.azul_claro; listaProdutos.refresh(); } }); } } if(ultimoIndicePressionado > indiceItem){ listaProdutos[ultimoIndicePressionado].corWidget = Colors.white; listaProdutos.refresh(); for(int i = 1; i <= ultimoIndicePressionado - indiceItem; i++){ int indexAlterar = ultimoIndicePressionado - i; double itemProgress = indexAlterar / (totalItems - 1); double timeRatio = findTimeForProgress(curve, itemProgress); int delay = (totalDurationMs * timeRatio).round(); Future.delayed(Duration(milliseconds: delay), () { if(indexAlterar > indiceItem){ listaProdutos[indexAlterar].corWidget = Colors.white; listaProdutos.refresh(); } }); } } }
方案二:监听动画帧实时同步颜色(推荐)
放弃Future.delayed,改用AnimationController+AnimatedBuilder监听动画每一帧进度,实时判断进度条是否覆盖选项,同步更新颜色。这种方式完全避免时间计算误差,还能响应动画中途的中断(比如用户重复点击按钮)。
1. 在Controller中配置动画控制器
class YourController extends GetxController with SingleTickerProviderStateMixin { late AnimationController _progressController; late Animation<double> _progressAnimation; final RxList<Produto> listaProdutos = <Produto>[].obs; int ultimoIndicePressionado = 0; @override void onInit() { super.onInit(); _progressController = AnimationController( duration: const Duration(seconds: 2), vsync: this, ); // 绑定动画曲线 _progressAnimation = CurvedAnimation( parent: _progressController, curve: Curves.slowMiddle, ); // 监听动画帧更新颜色 _progressAnimation.addListener(() { _syncItemColors(_progressAnimation.value); }); } // 根据当前进度同步选项颜色 void _syncItemColors(double currentProgress) { int totalItems = listaProdutos.length; double itemWidthRatio = 1 / (totalItems - 1); // spaceBetween布局的选项进度阈值 for (int i = 0; i < totalItems; i++) { double itemThreshold = i * itemWidthRatio; bool isCovered = currentProgress >= itemThreshold; if (ultimoIndicePressionado < widget.indiceItem) { // 正向移动:进度条覆盖的选项变蓝 listaProdutos[i].corWidget = isCovered ? res.azul_claro : Colors.white; } else { // 反向移动:进度条未覆盖的选项变白 listaProdutos[i].corWidget = isCovered ? Colors.white : res.azul_claro; } } listaProdutos.refresh(); } // 启动动画到目标进度 void startProgressAnimation(double targetProgress, int targetIndex) { ultimoIndicePressionado = targetIndex; _progressController.animateTo(targetProgress); } @override void onClose() { _progressController.dispose(); super.onClose(); } }
2. 修改按钮点击事件
onTap: (){ controller.startProgressAnimation(widget.valorProgress, widget.indiceItem); }
3. 替换AnimatedContainer为AnimatedBuilder
Obx(() => AnimatedBuilder( animation: controller._progressAnimation, builder: (context, child) { return Container( height: largura_total/28, child: Align( alignment: Alignment.centerLeft, child: Container( height: (largura_total/28)/3, width: (largura - 32) * controller._progressAnimation.value, decoration: BoxDecoration( color: res.azul_claro, borderRadius: BorderRadius.circular(14), ), ), ), ); }, ))
方案对比
- 方案一:基于时间延迟,适用于无法重构动画逻辑的场景,但仍可能存在微小误差,且无法处理动画中途中断的情况。
- 方案二:实时监听动画进度,同步精度最高,逻辑更清晰,能处理所有动画状态变化,是最优解。
内容的提问来源于stack exchange,提问作者Roger Walter
相关产品推荐
相关产品推荐

