Flutter動(dòng)畫設(shè)計(jì)原理

動(dòng)畫實(shí)現(xiàn)的方式

Flutter中荞估,我們可以簡單的把調(diào)用this.setState()理解為渲染一幀桐筏。那么只要我們不停的在調(diào)用這個(gè)方法的同時(shí)更新位置信息鸦泳,就能實(shí)現(xiàn)平移動(dòng)畫了船老,其他動(dòng)畫也是如此。

而Flutter也是這么做的。

這個(gè)方式說起來很簡單僧鲁,但是想要將它封裝成一個(gè)使用簡單的框架鼎姐,卻很不容易。

Flutter的實(shí)現(xiàn)

在Flutter中有兩大基類 Animatable 和 Animation

  • Animatable
    這個(gè)控制的是動(dòng)畫的類型的類茁影。例如平移動(dòng)畫我們關(guān)心的是x,y丧凤。那么Animatable就需要控制x募闲,y的變化。顏色動(dòng)畫我們關(guān)心的是色值得變化愿待,那么Animatable就需要控制色值浩螺。
    貝塞爾曲線運(yùn)動(dòng),我們關(guān)心的是路徑是按照貝塞爾方程式來生成x y仍侥,所以Animatable要有按照貝塞爾方程式的方式改變x要出,y。

  • Animation
    這個(gè)是控制動(dòng)畫運(yùn)動(dòng)過程的類农渊,不關(guān)心動(dòng)畫的類型厨幻。例如動(dòng)畫開始,停止,反轉(zhuǎn)况脆,還有各種ease得效果饭宾。
    并不關(guān)心你是平移,縮放還是貝塞爾曲線動(dòng)畫格了。因?yàn)樗械膭?dòng)畫這些狀態(tài)都是一樣的看铆。

假如我們想實(shí)現(xiàn)一個(gè)從0平移到200位置的動(dòng)畫該怎么做呢?

按照Flutter的實(shí)現(xiàn)方式我們要先要實(shí)現(xiàn)一個(gè)對(duì)應(yīng)的Animatable盛末。當(dāng)然flutter已經(jīng)為我門預(yù)制了很多類弹惦,Tween這個(gè)類就可已實(shí)現(xiàn)。
很多說Tween是補(bǔ)間動(dòng)畫悄但,自認(rèn)為很是不準(zhǔn)確棠隐。這里的Tween其實(shí)是一個(gè)一元一次函數(shù)的實(shí)現(xiàn)。簡單的說就是單個(gè)維度的漸變動(dòng)畫檐嚣。
如果要實(shí)現(xiàn)多維度的動(dòng)畫助泽,就需要自己實(shí)現(xiàn)Animatable。

  T lerp(double t) {
    assert(begin != null);
    assert(end != null);
    return begin + (end - begin) * t;
  }

  T transform(double t) {
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return lerp(t);
  }

有了Animatable嚎京,我們還需要一個(gè)Animation嗡贺,要不然怎么開始動(dòng)畫?

當(dāng)然Animation Flutter也為我們預(yù)制了鞍帝。AnimationController就是一個(gè)诫睬。

各種類都有了就開始寫代碼

class _SimpleRouteState extends State<SimpleRoute> with SingleTickerProviderStateMixin{
    ...

    AnimationController _controller;

    @override
    void initState() {
        _controller = AnimationController(
            duration : Duration(seconds: 1) ,
            vsync: this
        );
        ...
    }

    //點(diǎn)擊按鈕 開始動(dòng)畫
    _offsetAnim(bool isForward){
        Animation<double> animation = Tween(
            begin:0.0 ,
            end: 200.0).animate(_controller);
        animation.addListener((){
            this.setState((){
                this.left = animation.value;
            });
        });
        if(isForward){
            _controller.forward();
        }else{
            _controller.reverse();
        }
    }
}

上面的代碼先創(chuàng)建一個(gè)Animatable,然后調(diào)用animate()帕涌,傳入Animation
Animation 開始動(dòng)畫摄凡。
每刷一幀都會(huì)執(zhí)行一次

this.setState((){
    this.left = animation.value;
});

動(dòng)畫就實(shí)現(xiàn)了

動(dòng)畫實(shí)現(xiàn)原理

帶著幾個(gè)問題分析:
1.AnimationController在動(dòng)畫中扮演一個(gè)什么角色?
2.調(diào)用forward之后蚓曼,為什么動(dòng)畫就會(huì)開始亲澡?
3.是誰驅(qū)動(dòng)動(dòng)畫一直執(zhí)行,難道有for循環(huán)嗎辟躏?

AnimationController的角色

  • Overview

我的理解:AnimationController 將動(dòng)畫描述成一個(gè)可以量化的過程谷扣,這個(gè)量化的值就是從0.0到1.0的過程(采用默認(rèn)的下限值和上限值)土全。
從0.0到1.0就是forward , 從1.0到0.0就是reverse捎琐。而這個(gè)值就是_value這個(gè)變量。

  • 如何實(shí)現(xiàn)從0.0到1.0的過程裹匙?

通過Ticker來接受GPU的垂直同步信號(hào)瑞凑,在每次接受到信號(hào)后更新這個(gè)值。

//收到垂直信號(hào)后的回調(diào)處理方法
void _tick(Duration elapsed) {
    _lastElapsedDuration = elapsed;
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    assert(elapsedInSeconds >= 0.0);
    _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    if (_simulation.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    notifyListeners();
    _checkStatusChanged();
}

這也就是為什么構(gòu)建必須要傳入vsync的原因概页,AnimationController用他來創(chuàng)建一個(gè)Ticker的籽御。

  • forward之后做了啥?

先停止當(dāng)前正在進(jìn)行的動(dòng)畫,然后調(diào)用Ticker的start(),開始接受垂直同步的回調(diào)技掏,然后再回調(diào)中根據(jù)流失的時(shí)間铃将,來計(jì)算當(dāng)前的value值。
從而達(dá)到控制動(dòng)畫進(jìn)程的目的哑梳。

TickerFuture forward({ double from }) {
    ...
    //如果from沒有值劲阎,就默認(rèn)是動(dòng)畫到1.0(upperBound的默認(rèn)值是1.0)
    _direction = _AnimationDirection.forward;
    if (from != null)
      value = from;
    return _animateToInternal(upperBound);
}

//構(gòu)建一個(gè)Simulation
TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear, AnimationBehavior animationBehavior }) {
    ...

    if (simulationDuration == null) {
      ...
        //
      final double range = upperBound - lowerBound;
      final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
      simulationDuration = this.duration * remainingFraction;
    } else if (target == value) {
      // Already at target, don't animate.
      simulationDuration = Duration.zero;
    }
    stop();
    ...

    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
}

//開啟_ticker value開始從0.0到1.0變化。
TickerFuture _startSimulation(Simulation simulation) {

    ...
    _value = simulation.x(0.0).clamp(lowerBound, upperBound);
    final TickerFuture result = _ticker.start();
    ...

    return result;
  }

由此可見 AnimationController 在動(dòng)畫中扮演著控制動(dòng)畫過程的角色鸠真,通過維護(hù)著一個(gè)從0.0到1.0的變量來控制動(dòng)畫的進(jìn)行悯仙。

Tween的角色

  • Overview

    配合AnimationController,將一個(gè)變量從begin變化到end的過程吠卷。這個(gè)變化的過程是一個(gè)簡單的一元一次函數(shù)锡垄。
    t的取值范圍是[0,1]。若t是均勻變化的祭隔,就是線性的從begin到end货岭。

    T lerp(double t) {
      return begin + (end - begin) * t;
    }
    

    若這個(gè)變量是多個(gè)維度,例如是一個(gè)Rect序攘,有四個(gè)變量茴她,那就要從寫這個(gè)方法了〕痰欤可以參見RectTween丈牢。

  • Animation<T> animate(Animation parent) 這個(gè)方法做了啥?

這個(gè)就需要了解一個(gè)Flutter的私有類 _AnimatedEvaluation 它的父類是Animation瞄沙。
它起到了連接Tween和AnimationController的作用己沛,具體體現(xiàn)在它的get value的實(shí)現(xiàn)

@override
T get value => _evaluatable.evaluate(parent);

這個(gè)_evaluatable就是構(gòu)建_AnimatedEvaluation傳入的Tween,這句話會(huì)調(diào)用Tween的evaluate(),最終會(huì)調(diào)用上面的lerp();lerp參數(shù)t就是AnimationController維護(hù)的那個(gè)從0.0到1.0的變量距境。

當(dāng)你每次在setState()后調(diào)用animation.value申尼,就會(huì)走上面這個(gè)個(gè)方法,得到的就是當(dāng)前時(shí)間點(diǎn)的動(dòng)畫的值垫桂。這樣师幕,整個(gè)動(dòng)畫就被驅(qū)動(dòng)起來了。所以Flutter動(dòng)畫里面不存在for循環(huán)诬滩。

總結(jié)

* 如何用flutter實(shí)現(xiàn)一個(gè)貝塞爾曲線運(yùn)動(dòng)霹粥?

原理:繼承Animatable實(shí)現(xiàn)一個(gè)_BezierTween,并重寫他的transform();這里直接將貝塞爾曲線方程式帶進(jìn)去即可疼鸟。
當(dāng)然重寫Tween的lerp方法也是可行的后控。

bei.jpeg
i2lhs-vedko.gif

實(shí)現(xiàn)源碼:

class _Point{
  const _Point({this.x , this.y});

  final double x;
  final double y;
}

class _BezierTween extends Animatable<_Point>{
  _BezierTween({
    this.p0,
    this.p1,
    this.p2
  }):assert(p0 != null),
        assert(p1 != null),
        assert(p2 != null);

  final _Point p0; //起始點(diǎn)
  final _Point p1; //途徑點(diǎn)
  final _Point p2; //終點(diǎn)

  @override
  transform(double t) {
    double x = (1-t) * (1-t) * p0.x + 2 * t * (1-t) * p1.x + t * t * p2.x;
    double y = (1-t) * (1-t) * p0.y + 2 * t * (1-t) * p1.y + t * t * p2.y;
    return _Point(
        x:x ,
        y:y
    );
  }
}

使用

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(duration:Duration(seconds: 2) , vsync: this);

    _p0  = _Point( x:30,y:30);
    _p1  = _Point( x:30,y:200);
    _p2  = _Point( x:200,y:200);
    _animation = _BezierTween(p0: _p0 , p1: _p1 , p2: _p2).animate(_controller);
    _animation.addListener((){
      this.setState((){});
    });
  }

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市空镜,隨后出現(xiàn)的幾起案子浩淘,更是在濱河造成了極大的恐慌捌朴,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件张抄,死亡現(xiàn)場(chǎng)離奇詭異砂蔽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)署惯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門察皇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泽台,你說我怎么就攤上這事什荣。” “怎么了怀酷?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵稻爬,是天一觀的道長。 經(jīng)常有香客問我蜕依,道長桅锄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任样眠,我火速辦了婚禮友瘤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘檐束。我一直安慰自己辫秧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布被丧。 她就那樣靜靜地躺著盟戏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪甥桂。 梳的紋絲不亂的頭發(fā)上柿究,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音黄选,去河邊找鬼蝇摸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛办陷,可吹牛的內(nèi)容都是我干的貌夕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼懂诗,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蜂嗽!你這毒婦竟也來了苗膝?” 一聲冷哼從身側(cè)響起殃恒,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤植旧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后离唐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體病附,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年亥鬓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了完沪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嵌戈,死狀恐怖覆积,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熟呛,我是刑警寧澤宽档,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站庵朝,受9級(jí)特大地震影響吗冤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜九府,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一椎瘟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侄旬,春花似錦肺蔚、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至笔链,卻和暖如春段只,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鉴扫。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工赞枕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坪创。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓炕婶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親莱预。 傳聞我的和親對(duì)象是個(gè)殘疾皇子柠掂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容