Flutter动画设计原理

动画实现的方式

Flutter中,我们可以简单的把调用this.setState()理解为渲染一帧。那么只要我们不停的在调用这个方法的同时更新位置信息,就能实现平移动画了,其他动画也是如此。

而Flutter也是这么做的。

这个方式说起来很简单,但是想要将它封装成一个使用简单的框架,却很不容易。

Flutter的实现

在Flutter中有两大基类 Animatable 和 Animation

  • Animatable
    这个控制的是动画的类型的类。例如平移动画我们关心的是x,y。那么Animatable就需要控制x,y的变化。颜色动画我们关心的是色值得变化,那么Animatable就需要控制色值。
    贝塞尔曲线运动,我们关心的是路径是按照贝塞尔方程式来生成x y,所以Animatable要有按照贝塞尔方程式的方式改变x,y。

  • Animation
    这个是控制动画运动过程的类,不关心动画的类型。例如动画开始,停止,反转,还有各种ease得效果。
    并不关心你是平移,缩放还是贝塞尔曲线动画。因为所有的动画这些状态都是一样的。

假如我们想实现一个从0平移到200位置的动画该怎么做呢?

按照Flutter的实现方式我们要先要实现一个对应的Animatable。当然flutter已经为我门预制了很多类,Tween这个类就可已实现。
很多说Tween是补间动画,自认为很是不准确。这里的Tween其实是一个一元一次函数的实现。简单的说就是单个维度的渐变动画。
如果要实现多维度的动画,就需要自己实现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,我们还需要一个Animation,要不然怎么开始动画?

当然Animation Flutter也为我们预制了。AnimationController就是一个。

各种类都有了就开始写代码

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

    AnimationController _controller;

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

    //点击按钮 开始动画
    _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();
        }
    }
}

上面的代码先创建一个Animatable,然后调用animate(),传入Animation
Animation 开始动画。
每刷一帧都会执行一次

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

动画就实现了

动画实现原理

带着几个问题分析:
1.AnimationController在动画中扮演一个什么角色?
2.调用forward之后,为什么动画就会开始?
3.是谁驱动动画一直执行,难道有for循环吗?

AnimationController的角色

  • Overview

我的理解:AnimationController 将动画描述成一个可以量化的过程,这个量化的值就是从0.0到1.0的过程(采用默认的下限值和上限值)。
从0.0到1.0就是forward , 从1.0到0.0就是reverse。而这个值就是_value这个变量。

  • 如何实现从0.0到1.0的过程?

通过Ticker来接受GPU的垂直同步信号,在每次接受到信号后更新这个值。

//收到垂直信号后的回调处理方法
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();
}

这也就是为什么构建必须要传入vsync的原因,AnimationController用他来创建一个Ticker的。

  • forward之后做了啥?

先停止当前正在进行的动画,然后调用Ticker的start(),开始接受垂直同步的回调,然后再回调中根据流失的时间,来计算当前的value值。
从而达到控制动画进程的目的。

TickerFuture forward({ double from }) {
    ...
    //如果from没有值,就默认是动画到1.0(upperBound的默认值是1.0)
    _direction = _AnimationDirection.forward;
    if (from != null)
      value = from;
    return _animateToInternal(upperBound);
}

//构建一个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 在动画中扮演着控制动画过程的角色,通过维护着一个从0.0到1.0的变量来控制动画的进行。

Tween的角色

  • Overview

    配合AnimationController,将一个变量从begin变化到end的过程。这个变化的过程是一个简单的一元一次函数。
    t的取值范围是[0,1]。若t是均匀变化的,就是线性的从begin到end。

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

    若这个变量是多个维度,例如是一个Rect,有四个变量,那就要从写这个方法了。可以参见RectTween。

  • Animation<T> animate(Animation parent) 这个方法做了啥?

这个就需要了解一个Flutter的私有类 _AnimatedEvaluation 它的父类是Animation。
它起到了连接Tween和AnimationController的作用,具体体现在它的get value的实现

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

这个_evaluatable就是构建_AnimatedEvaluation传入的Tween,这句话会调用Tween的evaluate(),最终会调用上面的lerp();lerp参数t就是AnimationController维护的那个从0.0到1.0的变量。

当你每次在setState()后调用animation.value,就会走上面这个个方法,得到的就是当前时间点的动画的值。这样,整个动画就被驱动起来了。所以Flutter动画里面不存在for循环。

总结

* 如何用flutter实现一个贝塞尔曲线运动?

原理:继承Animatable实现一个_BezierTween,并重写他的transform();这里直接将贝塞尔曲线方程式带进去即可。
当然重写Tween的lerp方法也是可行的。

bei.jpeg
i2lhs-vedko.gif

实现源码:

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; //起始点
  final _Point p1; //途径点
  final _Point p2; //终点

  @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((){});
    });
  }

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容