目录
  • 前言
  • 实现步骤
    • 1、绘制静态效果
    • 2、加入动画
    • 两个关键点
  • 完整源码
    • 总结

      前言

      牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。

      – 知识点:绘制、动画曲线、多动画状态更新

      效果图:

      Flutter实现牛顿摆动画效果的示例代码

      实现步骤

      1、绘制静态效果

      首先我们需要把线和小圆球绘制出来,对于看过我之前文章的小伙伴来说这个就很简单了,效果图:

      Flutter实现牛顿摆动画效果的示例代码

      关键代码:

      // 小圆球半径
      double radius = 6;
      
      /// 小球圆心和直线终点一致
      //左边小球圆心
      Offset offset = Offset(20, 60);
      //右边小球圆心
      Offset offset2 = Offset(20 * 6 * 8, 60);
      
      Paint paint = Paint()
        ..color = Colors.black87
        ..strokeWidth = 2;
      
      /// 绘制线
      canvas.drawLine(Offset.zero, Offset(90, 0), paint);
      canvas.drawLine(Offset(20, 0), offset, paint);
      canvas.drawLine(
          Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
      canvas.drawLine(
          Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
      canvas.drawLine(
          Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
      canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);
      
      /// 绘制小圆球
      canvas.drawCircle(offset, radius, paint);
      canvas.drawCircle(Offset(20 + radius * 2, 60), radius, paint);
      canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
      canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
      canvas.drawCircle(offset2, radius, paint);

      2、加入动画

      思路: 我们可以看到5个小球一共2个小球在运动,左边小球运动一个来回之后传递给右边小球,右边小球开始运动,右边一个来回再传递给左边开始,也就是左边运动周期是:0-1-0,正向运动一次,反向再运动一次,这样就是一个周期,右边也是一样,左边运动完传递给右边,右边运动完传递给左边,这样就简单实现了牛顿摆的效果。

      两个关键点

      小球运动路径: 小球的运动路径是一个弧度,以竖线的起点为圆心,终点为半径,那么我们只需要设置小球运动至最高点的角度即可,通过角度就可计算出小球的坐标点。

      运动曲线: 当然我们知道牛顿摆小球的运动曲线并不是匀速的,他是有一个加速减速过程的,撞击之后,小球先加速然后减速达到最高点速度为0,之后速度再从0慢慢加速进行撞击小球,周而复始。

      下面的运动曲线就是先加速再减速,大概符合牛顿摆的运动曲线。我们就使用这个曲线看看效果。

      Flutter实现牛顿摆动画效果的示例代码

      完整源码

      class OvalLoading extends StatefulWidget {
        const OvalLoading({Key? key}) : super(key: key);
      
        @override
        _OvalLoadingState createState() => _OvalLoadingState();
      }
      
      class _OvalLoadingState extends State<OvalLoading>
          with TickerProviderStateMixin {
        // 左边小球
        late AnimationController _controller =
            AnimationController(vsync: this, duration: Duration(milliseconds: 300))
              ..addStatusListener((status) {
                if (status == AnimationStatus.completed) {
                  _controller.reverse(); //反向执行 1-0
                } else if (status == AnimationStatus.dismissed) {
                  _controller2.forward();
                }
              })
              ..forward();
        // 右边小球
        late AnimationController _controller2 =
            AnimationController(vsync: this, duration: Duration(milliseconds: 300))
              ..addStatusListener((status) {
                // dismissed 动画在起始点停止
                // forward 动画正在正向执行
                // reverse 动画正在反向执行
                // completed 动画在终点停止
                if (status == AnimationStatus.completed) {
                  _controller2.reverse(); //反向执行 1-0
                } else if (status == AnimationStatus.dismissed) {
                  // 反向执行完毕左边小球执行
                  _controller.forward();
                }
              });
        late var cure =
            CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
        late var cure2 =
            CurvedAnimation(parent: _controller2, curve: Curves.easeOutCubic);
      
        late Animation<double> animation = Tween(begin: 0.0, end: 1.0).animate(cure);
      
        late Animation<double> animation2 =
            Tween(begin: 0.0, end: 1.0).animate(cure2);
      
        @override
        Widget build(BuildContext context) {
          return Container(
            margin: EdgeInsetsDirectional.only(top: 300, start: 150),
            child: CustomPaint(
              size: Size(100, 100),
              painter: _OvalLoadingPainter(
                  animation, animation2, Listenable.merge([animation, animation2])),
            ),
          );
        }
      
        @override
        void dispose() {
          _controller.dispose();
          _controller2.dispose();
          super.dispose();
        }
      }
      
      class _OvalLoadingPainter extends CustomPainter {
        double radius = 6;
        final Animation<double> animation;
        final Animation<double> animation2;
        final Listenable listenable;
      
        late Offset offset; // 左边小球圆心
        late Offset offset2; // 右边小球圆心
      
        final double lineLength = 60; // 线长
      
        _OvalLoadingPainter(this.animation, this.animation2, this.listenable)
            : super(repaint: listenable) {
          offset = Offset(20, lineLength);
          offset2 = Offset(20 * radius * 8, lineLength);
        }
      
        // 摆动角度
        double angle = pi / 180 * 30; // 30°
      
        @override
        void paint(Canvas canvas, Size size) {
          Paint paint = Paint()
            ..color = Colors.black87
            ..strokeWidth = 2;
      
          // 左边小球 默认坐标 下方是90度 需要+pi/2
          var dx = 20 + 60 * cos(pi / 2 + angle * animation.value);
          var dy = 60 * sin(pi / 2 + angle * animation.value);
          // 右边小球
          var dx2 = 20 + radius * 8 - 60 * cos(pi / 2 + angle * animation2.value);
          var dy2 = 60 * sin(pi / 2 + angle * animation2.value);
      
          offset = Offset(dx, dy);
          offset2 = Offset(dx2, dy2);
      
          /// 绘制线
            canvas.drawLine(Offset.zero, Offset(90, 0), paint);
          canvas.drawLine(Offset(20, 0), offset, paint);
          canvas.drawLine(
              Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
          canvas.drawLine(
              Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
          canvas.drawLine(
              Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
          canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);
      
          /// 绘制球
          canvas.drawCircle(offset, radius, paint);
          canvas.drawCircle(
              Offset(20 + radius * 2, 60),
              radius,
              paint);
      
          canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
          canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
          canvas.drawCircle(offset2, radius, paint);
        }
        @override
        bool shouldRepaint(covariant _OvalLoadingPainter oldDelegate) {
          return oldDelegate.listenable != listenable;
        }
      }

      去掉线的效果

      Flutter实现牛顿摆动画效果的示例代码

      总结

      本文展示了实现牛顿摆的原理,其实并不复杂,关键点就是小球的运动轨迹和运动速度曲线,如果用到项目中当做Loading还有很多优化的空间,比如加上小球影子、修改小球颜色或者把小球换成好玩的图片等等操作会看起来更好看一点

      声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。