Flutter电容充放电动画优化:实现曲线衔接切换
Flutter电容充放电动画曲线平滑衔接实现方案
我用Flutter的CustomPaint和AnimationController实现电容充放电动画,充放电各对应专属公式,但点击切换按钮时,新曲线总是从初始点重新绘制,无法从上一次曲线的终点平滑衔接。当前效果为切换后曲线从头开始,目标是实现切换后曲线从上次终点继续绘制的无缝过渡效果。
修改后的完整代码
import 'dart:math'; import 'package:flutter/material.dart'; class Trace extends StatefulWidget { const Trace({Key? key}) : super(key: key); @override State<Trace> createState() => _TraceState(); } class _TraceState extends State<Trace> with TickerProviderStateMixin { late AnimationController animation; bool isCharging = true; Duration duration = const Duration(seconds: 20); // 保存当前绘制的终点坐标和对应的x轴位置 double currentEndT = 0; double currentEndY = 0; // 记录初始的x轴起点 double initialStartT = 0; @override void didChangeDependencies() { super.didChangeDependencies(); double screenWidth = MediaQuery.of(context).size.width; double x = screenWidth / 2; initialStartT = x - 80; currentEndT = initialStartT; currentEndY = (MediaQuery.of(context).size.height / 2) - 50 - 50; double lowerBound = (x - 80) / (x + 82); animation = AnimationController( vsync: this, duration: duration, lowerBound: lowerBound, upperBound: 1, )..addListener(() { setState(() { // 实时更新当前终点坐标 double screenWidth = MediaQuery.of(context).size.width; double x = screenWidth / 2; double tMax = (x + 82) * animation.value; double vo = 100; double c = 2 * 0.000001; double r = 500000 * 10; double yBase = (MediaQuery.of(context).size.height / 2) - 50 - 50; if (isCharging) { currentEndY = yBase - vo * (1 - exp((-(tMax - initialStartT) / (r * c)))); } else { currentEndY = yBase - vo * (exp((-(tMax - initialStartT) / (r * c)))); } currentEndT = tMax; }); }); animation.forward(); } @override Widget build(BuildContext context) { return Scaffold( floatingActionButton: FloatingActionButton(onPressed: () { setState(() { isCharging = !isCharging; // 切换时基于当前进度重置动画控制器 double screenWidth = MediaQuery.of(context).size.width; double x = screenWidth / 2; double totalRange = (x + 82) - initialStartT; double currentProgress = (currentEndT - initialStartT) / totalRange; animation.stop(); animation.reset(); animation.value = currentProgress; animation.forward(); }); }), backgroundColor: Colors.black, body: InteractiveViewer( minScale: 0.5, maxScale: 5, boundaryMargin: const EdgeInsets.all(double.infinity), child: Container( alignment: Alignment.center, width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: CustomPaint( painter: TracePainter( animation, isCharging, initialStartT, currentEndT, currentEndY ), child: Container(), ), ), ), ); } } class TracePainter extends CustomPainter { TracePainter( this.animation, this.isCharging, this.initialStartT, this.currentEndT, this.currentEndY ); final Animation animation; final bool isCharging; final double initialStartT; final double currentEndT; final double currentEndY; @override void paint(Canvas canvas, Size size) { final Paint paint = Paint() ..color = const Color.fromARGB(255, 89, 236, 255) ..style = PaintingStyle.stroke ..strokeWidth = 1.4; final Paint paintRect = Paint() ..color = Colors.grey ..style = PaintingStyle.stroke ..strokeWidth = 0.5; double screenWidth = size.width; double screenheight = size.height; double x = screenWidth / 2; double y = (screenheight / 2) - 50; final path = Path(); double vo = 100; double c = 2 * 0.000001; double r = 500000 * 10; double yBase = y - 50; // 绘制从初始点到当前终点的历史路径 path.moveTo(initialStartT, yBase); double t = initialStartT; for (; t <= currentEndT; t += 0.01) { double out = isCharging ? yBase - vo * (1 - exp((-(t - initialStartT) / (r * c)))) : yBase - vo * (exp((-(t - initialStartT) / (r * c)))); path.lineTo(t, out); } // 从当前终点开始绘制新的动画部分 double tNew = currentEndT; double tMax = (x + 82) * animation.value; for (; tNew <= tMax; tNew += 0.01) { double out = isCharging ? currentEndY - vo * (1 - exp((-(tNew - currentEndT) / (r * c)))) : currentEndY - vo * (exp((-(tNew - currentEndT) / (r * c)))); path.lineTo(tNew, out); } canvas.drawPath(path, paint); canvas.drawRect( Rect.fromCenter(center: Offset(x, y - 100), width: 164, height: 104), paintRect); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; } }
关键修改说明
- 状态变量保存:新增
currentEndT、currentEndY记录当前曲线的终点坐标,initialStartT记录初始x轴起点,为切换后的曲线计算提供基准。 - 动画控制器适配:切换充放电时,先停止当前动画,重置控制器并将进度设置为已绘制的比例,再继续动画,避免从头播放。
- 分段绘制逻辑:绘制时先完成从初始点到当前终点的历史路径,再以当前终点为新起点,代入对应充放电公式绘制后续曲线,确保衔接平滑。
- 实时终点更新:在动画监听器中实时计算并更新当前终点坐标,保证切换时能获取到最新的位置参数。
内容的提问来源于stack exchange,提问作者Ahm2025
相关产品推荐
相关产品推荐

