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

Flutter指南针物理动画实现:SpringSimulation参数调优求助

指南针物理模拟动画参数调整方案

我希望让CustomPaint中的PathL1实现类似指南针摆动的物理模拟动画(目标效果:指针从初始位置摆动到目标角度,伴随自然的阻尼震荡),但找不到合适的SpringSimulation参数(mass、stiffness、damping等)以及AnimationController的配置,以下是我的代码:

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
class Trace extends StatefulWidget {
  const Trace({Key? key}) : super(key: key);
  @override
  State<Trace> createState() => _TraceState();
}
@override
class _TraceState extends State<Trace> with TickerProviderStateMixin {
  late AnimationController animation;
  late SpringSimulation simulation;
  Duration duration = const Duration(seconds: 1);
  @override
  void initState() {
    super.initState();
    animation = AnimationController(
      vsync: this,
      duration: duration,
      lowerBound: 0,
      upperBound: pi,
    )..addListener(() {
        setState(() {});
      });
    animation.forward();
    simulation = SpringSimulation(
      const SpringDescription(mass: 0.5, stiffness: 10, damping: 500),
      0,
      50,
      0,
    );
    animation.animateWith(simulation);
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: CustomPaint(
        painter: TracePainter(animation),
        child: Container(),
      ),
    );
  }
}
class TracePainter extends CustomPainter {
  TracePainter(this.animation);
  final Animation animation;
  @override
  void paint(Canvas canvas, Size size) {
    double w = size.width;
    double h = size.height;
    final paintcii = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4;
    final paintc = Paint()..color = Colors.grey.withOpacity(0.3);
    canvas.drawLine(
        Offset(size.width / 2, size.height), Offset(size.width / 2, 0), paintc);
    canvas.drawLine(Offset(0, size.height / 2),
        Offset(size.width, size.height / 2), paintc);
    final pathL1 = Path()
      ..moveTo(w / 2 - 35 * cos(animation.value),
          (h / 2) - 35 * sin(animation.value))
      ..lineTo(w / 2 + 35 * cos(animation.value),
          (h / 2) + 35 * sin(animation.value));
    canvas.drawPath(pathL1, paintcii);
  }
  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

当前效果:指针直接生硬地移动到目标位置,没有自然的摆动震荡效果。


核心问题分析

  • AnimationController配置冲突:手动设置duration和upperBound: pi会限制弹簧运动的范围和时长,SpringSimulation会自主控制动画节奏,无需这些限制。
  • SpringSimulation参数错误:目标值设为50不符合角度逻辑,指南针角度需用弧度值;mass、stiffness、damping参数搭配不合理,导致没有自然摆动效果。
  • 动画调用重复:同时调用animation.forward()和animation.animateWith(simulation)会导致冲突,后者会覆盖前者的动画逻辑。

修改后的完整代码

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/physics.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;
  late SpringSimulation simulation;

  @override
  void initState() {
    super.initState();
    animation = AnimationController(
      vsync: this,
      lowerBound: double.negativeInfinity,
      upperBound: double.infinity, // 放开角度限制,让弹簧自由运动
    )..addListener(() {
        setState(() {});
      });

    // 模拟指南针从初始角度0(指向右)摆动到目标角度-pi/2(指向上,北方)
    simulation = SpringSimulation(
      const SpringDescription(
        mass: 0.8,      // 较小质量,让指针摆动更轻盈
        stiffness: 120, // 适中刚度,保证归位速度合理
        damping: 12,    // 适中阻尼,让摆动逐步衰减
      ),
      0,               // 初始角度(弧度)
      -pi/2,           // 目标角度(指向上方)
      0,               // 初始速度
    );
    animation.animateWith(simulation);
  }

  @override
  void dispose() {
    animation.dispose(); // 释放控制器,避免内存泄漏
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: CustomPaint(
        painter: TracePainter(animation),
        child: const SizedBox.expand(), // 让CustomPaint占满整个屏幕
      ),
    );
  }
}

class TracePainter extends CustomPainter {
  TracePainter(this.animation);
  final Animation<double> animation;

  @override
  void paint(Canvas canvas, Size size) {
    double w = size.width;
    double h = size.height;
    final center = Offset(w/2, h/2);

    final paintcii = Paint()
      ..color = Colors.grey
      ..style = PaintingStyle.stroke
      ..strokeWidth = 4;

    final paintc = Paint()..color = Colors.grey.withOpacity(0.3);

    // 绘制十字参考线
    canvas.drawLine(Offset(w/2, 0), Offset(w/2, h), paintc);
    canvas.drawLine(Offset(0, h/2), Offset(w, h/2), paintc);

    // 计算指针的两个端点
    final end1 = center + Offset(-35 * cos(animation.value), -35 * sin(animation.value));
    final end2 = center + Offset(35 * cos(animation.value), 35 * sin(animation.value));

    final pathL1 = Path()
      ..moveTo(end1.dx, end1.dy)
      ..lineTo(end2.dx, end2.dy);
    canvas.drawPath(pathL1, paintcii);
  }

  @override
  bool shouldRepaint(covariant TracePainter oldDelegate) {
    return animation.value != oldDelegate.animation.value;
  }
}

参数说明

  • mass: 设为0.8,质量越小,指针响应越快,摆动质感越轻盈。
  • stiffness: 设为120,刚度决定弹簧的"弹力",值越大,指针归位速度越快。
  • damping: 设为12,阻尼决定摆动衰减速度,值过大摆动会直接停止,值过小会导致多次无意义震荡。
  • 目标角度: -pi/2对应Flutter坐标系的顶部(北方),如需指向其他方向,可调整弧度值:pi/2指向下,0指向右,pi指向左。

额外优化点

  1. 添加animation.dispose(),避免内存泄漏。
  2. 将child: Container()改为const SizedBox.expand(),让CustomPaint自动占满屏幕。
  3. 优化shouldRepaint方法,仅在动画值变化时重绘,提升性能。

内容的提问来源于stack exchange,提问作者Ahm2025

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 13:26:20