Flutter動畫之粒子精講

張風捷特烈發表於2019-07-16

本文所有原始碼見github/flutter_journey

1.何為動畫

Flutter動畫之粒子精講

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來實現一個不斷重新整理的舞臺,那麼表演就交給你了

Flutter動畫之粒子精講

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影象,隨著每次更新,根據函式關係約束小球座標值

Flutter動畫之粒子精講

  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;
  }
複製程式碼

或者讓小球按圓形軌跡運動,下面是通過引數方程讓呈圓形軌跡
也就是數學學得好,想怎麼跑怎麼跑。

Flutter動畫之粒子精講

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

3.粒子束

3.1:多個粒子運動

一個粒子運動已經夠好玩的,那麼許多粒子會怎麼樣?
需要改變的是RunBallView的入參,由一個球換成小球列表,
繪畫時批量繪製,更新資訊時批量更新

Flutter動畫之粒子精講

//[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:撞擊分裂的效果

也就是在恰當的時機可以新增粒子而達到一定的視覺效果
核心是當到達邊界後進行處理,將原來的粒子半徑減半,再新增一個等大反向的粒子

Flutter動畫之粒子精講

//限定下邊界
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:特定粒子

現在可以感受到,動畫就是元素的資訊在不斷變化,給人產生的感覺
只要將資訊描述好,那麼你可以完成任何動畫,你就是創造者與主宰者

Flutter動畫之粒子精講

點陣分析.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畫素級操作 寫得很資訊,這裡不展開了

Flutter動畫之粒子精講

總的來說,動畫包括三個重要的條件時間流,渲染繪製,資訊更新邏輯
這並不只是對於Flutter,任何語言只要滿足這三點,粒子動畫就可以跑起來
至於有什麼用,也許可以提醒我,我不是搬磚的,而是程式設計師一個Creater...


結語

本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。

本文所有原始碼見github/flutter_journey

相關文章