Flutter动画之粒子精讲

作者: 张风捷特烈 | 来源:发表于2019-07-16 16:28 被阅读8次

    本文所有源码见github/flutter_journey

    1.何为动画

    image
    1.1:动画说明

    见字如面,会动的画面。画面连续渲染,当速度快到一定程度,大脑就会呈现动感

    1).何为运动:视觉上看是一个物体在不同的时间轴上表现出不同的物理位置
    2).位移 = 初位移 + 速度 * 时间 小学生的知识不多说
    3).速度 = 初速度 + 加速度 * 时间 初中生的知识不多说
    4).时间、位移、速度、加速度构成了现代科学的运动体系
    

    1.2:关于FPS

    那刷新要有多快呢?不知你是否听过FPS,对就是那个游戏里很重要的FPS

    FPS : Frames Per Second  画面每秒传输帧数(新率) 单位赫兹(Hz)
    60Hz的刷新率刷也就是指屏幕一秒内刷新60次,即60帧/秒 
    
    其中常见的电影24fps,也就是一秒钟刷新24次。
    要达到流畅,需要60fps,这也是游戏中的一个指标,否则就会感觉不流畅  
    一秒钟刷新60次,即16.66667ms刷新一次,这也是一个常见的值
    

    1.3:代码中的动画

    可以用代码模拟运动,不断刷新的同时改变运动物体的属性从而形成动画
    在Android中有ValueAnimator,JavaScript(浏览器)中有``.

    1.时间:无限执行----模拟时间流,每次刷新时间间隔,记为:1T
    2.位移:物体在屏幕像素位置----模拟世界,每个像素距离记为:1px
    3.速度(单位px/T)、加速度(px/T^2)
    注意:无论什么语言,只要能够模拟时间与位移,本篇的思想都可以适用,只是语法不同罢了
    

    2.粒子动画

    2.1:Flutter中的时间流

    通过AnimationController来实现一个不断刷新的舞台,那么表演就交给你了

    image
    class RunBall extends StatefulWidget {
      @override
      _RunBallState createState() => _RunBallState();
    }
    
    class _RunBallState extends State<RunBall> with SingleTickerProviderStateMixin {
      AnimationController controller;
      var _oldTime = DateTime.now().millisecondsSinceEpoch;//首次运行时时间
    
      @override
      Widget build(BuildContext context) {
        var child = Scaffold(
        );
    
        return GestureDetector(//手势组件,做点击响应
          child: child,
          onTap: () {
            controller.forward();//执行动画
          },
        );
      }
    
      @override
      void initState() {
        controller =//创建AnimationController对象
            AnimationController(duration: Duration(days: 999 * 365), vsync: this);
        controller.addListener(() {//添加监听,执行渲染
          _render();
        });
      }
    
      @override
      void dispose() {
        controller.dispose(); // 资源释放
      }
    
      //渲染方法,更新状态
      _render() {
        setState(() {
          var now = DateTime.now().millisecondsSinceEpoch;//每一刷新时间
          print("时间差:${now - _oldTime}ms");//打印时间差
          _oldTime = now;//重新赋值
        });
      }
    }
    

    2.2:静态小球的绘制

    又到了我们的Canvas了

    小球.png
    ///小球信息描述类
    class Ball {
      double aX; //加速度
      double aY; //加速度Y
      double vX; //速度X
      double vY; //速度Y
      double x; //点位X
      double y; //点位Y
      Color color; //颜色
      double r;//小球半径
    
      Ball({this.x=0, this.y=0, this.color, this.r=10,
            this.aX=0, this.aY=0, this.vX=0, this.vY=0});
    }
    
    ///画板Painter
    class RunBallView extends CustomPainter {
      Ball _ball; //小球
      Rect _area;//运动区域
      Paint mPaint; //主画笔
      Paint bgPaint; //背景画笔
    
      RunBallView(this._ball,this._area) {
        mPaint = new Paint();
        bgPaint = new Paint()..color = Color.fromARGB(148, 198, 246, 248);
      }
    
      @override
      void paint(Canvas canvas, Size size) {
        canvas.drawRect(_area, bgPaint);
        _drawBall(canvas, _ball);
      }
    
      @override
      bool shouldRepaint(CustomPainter oldDelegate) {
        return true;
      }
    
      ///使用[canvas] 绘制某个[ball]
      void _drawBall(Canvas canvas, Ball ball) {
        canvas.drawCircle(
            Offset(ball.x, ball.y), ball.r, mPaint..color = ball.color);
      }
    }
    
    var _area= Rect.fromLTRB(0+40.0,0+200.0,280+40.0,200+200.0);
    var _ball = Ball(color: Colors.blueAccent, r: 10,x: 40.0+140,y:200.0+100);
    
    ---->[使用:_RunBallState#build]----
    var child = Scaffold(
      body: CustomPaint(
        painter: RunBallView(_ball,_area),
      ),
    );
    

    2.3:远动盒

    也就是控制小球在每次刷新时改变其属性,这样视觉上就是运动状态
    在边界碰撞后,改变方向即可,通过下面三步,一个运动盒就完成了

    速度的合成.png 碰撞分析png 运动盒.gif
    //[1].为小球附上初始速度和加速度
    var _ball = Ball(color: Colors.blueAccent, r: 10,aY: 0.1, vX: 2, vY: -2,x: 40.0+140,y:200.0+100);
    
    //[2].核心渲染方法,每次调用时更新小球信息
      _render() {
        updateBall();
        setState(() {
          var now = DateTime.now().millisecondsSinceEpoch;
          print("时间差:${now - _oldTime}ms,帧率:${1000/(now - _oldTime)}");
          _oldTime = now;
        });
      }
      
    //[3].更新小球的信息
      void updateBall() {
        //运动学公式
        _ball.x += _ball.vX;
        _ball.y += _ball.vY;
        _ball.vX += _ball.aX;
        _ball.vY += _ball.aY;
        //限定下边界
        if (_ball.y > _area.bottom - _ball.r) {
          _ball.y = _area.bottom - _ball.r;
          _ball.vY = -_ball.vY;
          _ball.color=randomRGB();//碰撞后随机色
        }
        //限定上边界
        if (_ball.y < _area.top + _ball.r) {
          _ball.y = _area.top + _ball.r;
          _ball.vY = -_ball.vY;
          _ball.color=randomRGB();//碰撞后随机色
        }
    
        //限定左边界
        if (_ball.x < _area.left + _ball.r) {
          _ball.x = _area.left + _ball.r;
          _ball.vX = -_ball.vX;
          _ball.color=randomRGB();//碰撞后随机色
        }
    
        //限定右边界
        if (_ball.x > _area.right - _ball.r) {
          _ball.x = _area.right - _ball.r;
          _ball.vX= -_ball.vX;
          _ball.color=randomRGB();//碰撞后随机色
        }
      }
    }
    

    2.4:让小球按照指定的函数图像运动

    给定一个较小的dx,随着dx增加,根据函数求出dy,然后更新小球信息
    如下面的sin图像,随着每次更新,根据函数关系约束小球坐标值

    image
      double dx=0.0;
      void updateBall(){
        dx+=pi/180;//每次dx增加pi/180
        _ball.x+=dx;
        _ball.y+=f(dx);
      }
    
      f(x){
        var y= 5*sin(4*x);//函数表达式
        return y;
      }
    

    或者让小球按圆形轨迹运动,下面是通过参数方程让呈圆形轨迹
    也就是数学学得好,想怎么跑怎么跑。

    image
      double dx=0.0;
      void updateBall(){
        dx+=pi/180;//每次dx增加pi/180
        _ball.x+=cos(dx);
        _ball.y+=sin(dx);
      }
    

    3.粒子束

    3.1:多个粒子运动

    一个粒子运动已经够好玩的,那么许多粒子会怎么样?
    需要改变的是RunBallView的入参,由一个球换成小球列表,
    绘画时批量绘制,更新信息时批量更新

    image
    //[1].单体改成列表
    class RunBallView extends CustomPainter {
      List<Ball> _balls; //小球列表
      
    //[2].绘画时批量绘制
      void paint(Canvas canvas, Size size) {
        _balls.forEach((ball) {
          _drawBall(canvas, ball);
        });
      }
    
    //[3].渲染时批量更改信息
    _render() {
      for (var i = 0; i < _balls.length; i++) {
        updateBall(i);
      }
      setState(() {
      });
    }
    
    //[4]._RunBallState中初始化时生成随机信息的小球
    for (var i = 0; i < 30; i++) {
      _balls.add(Ball(
          color: randomRGB(),
          r: 5 + 4 * random.nextDouble(),
          vX: 3*random.nextDouble()*pow(-1, random.nextInt(20)),
          vY:  3*random.nextDouble()*pow(-1, random.nextInt(20)),
          aY: 0.1,
          x: 200,
          y: 300));
    }
    

    也许你觉得画小球没什么,但要知道,小球只是单体,
    你可以换成任意你能绘制的东西,甚至是图片或组件


    3.2:撞击分裂的效果

    也就是在恰当的时机可以添加粒子而达到一定的视觉效果
    核心是当到达边界后进行处理,将原来的粒子半径减半,再添加一个等大反向的粒子

    image
    //限定下边界
    if (ball.y > _area.bottom) {
      var newBall = Ball.fromBall(ball);
      newBall.r = newBall.r / 2;
      newBall.vX = -newBall.vX;
      newBall.vY = -newBall.vY;
      _balls.add(newBall);
      ball.r = ball.r / 2;
    
      ball.y = _area.bottom;
      ball.vY = -ball.vY;
      ball.color = randomRGB(); //碰撞后随机色
    }
    

    当越分越多时,会存在大量绘制,这时可以控制一下条件来移除

    void updateBall(int i) {
       var ball = _balls[i];
       if (ball.r < 0.3) {
         //半径小于0.3就移除
         _balls.removeAt(i);
       }
      //略...
    }
    

    3.3:特定粒子

    现在可以感受到,动画就是元素的信息在不断变化,给人产生的感觉
    只要将信息描述好,那么你可以完成任何动画,你就是创造者与主宰者

    image 点阵分析.png
    /**
     * 渲染数字
     * @param num    要显示的数字
     * @param canvas 画布
     */
    void renderDigit(double radius) {
      var one = [
        [0, 0, 0, 1, 1, 0, 0],
        [0, 1, 1, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 0],
        [0, 0, 0, 1, 1, 0, 0],
        [1, 1, 1, 1, 1, 1, 1]
      ]; //1
      for (int i = 0; i < one.length; i++) {
        for (int j = 0; j < one[j].length; j++) {
          if (one[i][j] == 1) {
            double rX = j * 2 * (radius + 1) + (radius + 1); //第(i,j)个点圆心横坐标
            double rY = i * 2 * (radius + 1) + (radius + 1); //第(i,j)个点圆心纵坐标
            _balls.add(Ball(
                r: radius,
                x: rX,
                y: rY,
                color: randomRGB(),
                vX: 3 * random.nextDouble() * pow(-1, random.nextInt(20)),
                vY: 3 * random.nextDouble() * pow(-1, random.nextInt(20))));
          }
        }
      }
    }
    

    通过一个二维数组记录点位信息,在绘制的时候判断绘制就能呈现既定效果
    然后通过信息创建小球,通过渲染展现出来,通过动画将其运动。
    其实通过像素点也可以记录这些信息,就可以将图片进行粒子画,
    之前在Android粒子篇之Bitmap像素级操作 写得很信息,这里不展开了

    image

    总的来说,动画包括三个重要的条件时间流,渲染绘制,信息更新逻辑
    这并不只是对于Flutter,任何语言只要满足这三点,粒子动画就可以跑起来
    至于有什么用,也许可以提醒我,我不是搬砖的,而是程序设计师一个Creater...


    结语

    本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
    另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328,期待与你的交流与切磋。

    本文所有源码见github/flutter_journey

    相关文章

      网友评论

        本文标题:Flutter动画之粒子精讲

        本文链接:https://www.haomeiwen.com/subject/ytptlctx.html