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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 13:28:11