Flutter 中的 Animations(三)

劉斯龍發表於2018-08-20

官方文件

如果沒有看過之前兩節的,建議先看看前兩節的內容

上一節中我們主要做的事情如下:
  • AnimationaddListener(...) 中呼叫 setState(...) 來給 widget 新增動畫
  • 使用 AnimatedWidget 的方法來給 widget 新增動畫

上一節中我們也通過案例的形式展現出以上兩種方式的區別,得出的結論就是如下: 使用 addListener(...)setState(...)Widget 新增動畫的時候,會導致其他的 Widget 也跟著重繪,而使用 AnimatedWidget 的方式給 Widget 新增動畫的時候只會重繪當前的 Widget

這是為什麼呢 ???

我們先來看看動畫的執行流程。

我們在使用動畫的時候會初始化一個 AnimationController

AnimationController controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300));
複製程式碼

可以看出,初始化 AnimationController 需要傳遞兩個引數,一個是 vsync 一個是 duration ,這個 duration 很容易理解,就是動畫執行完畢需要的時長,那 vsync 是什麼啊?上述程式碼中我們傳入了 this ,那是因為我們在 Statewith(混入,第一節 有講)SingleTickerProviderStateMixin,下面我們看看初始化 AnimationController 時都幹了什麼事,原始碼如下:

AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound: 0.0,
    this.upperBound: 1.0,
    @required TickerProvider vsync,
  }) : assert(lowerBound != null),
       assert(upperBound != null),
       assert(upperBound >= lowerBound),
       assert(vsync != null),
       _direction = _AnimationDirection.forward {
    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
  }
複製程式碼

我們抽重點出來看

_ticker = vsync.createTicker(_tick);
複製程式碼

這裡呼叫了 vsynccreateTicker(...) 方法建立了一個 Ticker, 而我們在初始化 AnimationController 的時候 vsync 引數傳的是 SingleTickerProviderStateMixinSingleTickerProviderStateMixin#createTicker(...) 的原始碼如下:

Ticker createTicker(TickerCallback onTick) {

    ...
    
    _ticker = new Ticker(onTick, debugLabel: 'created by $this');
    
    ...
    
    return _ticker;
  }
複製程式碼

這個 Ticker 是個什麼東西呢?看看它的註釋吧!以下為 Ticker 類的部分註釋

Calls its callback once per animation frame.

When created, a ticker is initially disabled. Call [start] to enable the ticker.

Creates a ticker that will call the provided callback once per frame while running.

  • 第一句的大概意思就是 Ticker 類在每一幀動畫中都會呼叫他自己的回撥一次
  • 第二句的大概意思就是 當 Ticker 被建立之後,預設是 不可用(disabled)的,可以呼叫 start 方法來使 Ticker 變得可用(enable)
  • 第三句的大概意思就是 建立一個 Ticker,他將在執行時的每一幀都會呼叫一次所提供的回撥

建立 Ticker 到這兒就完了。 如何讓Ticker可用呢,也就是如何呼叫它的 start 方法呢?

我們在啟動動畫的時候會呼叫 AnimationController#forward() , 在 forward() 方法中就間接的會呼叫到 Ticker#start()Ticker#start() 最後會呼叫到 scheduleFrame() ,而這個方法裡面呼叫了 ui.window.scheduleFrame(); ,這個 scheduleFrame() 是一個 Native 方法,如下:

void scheduleFrame() native 'Window_scheduleFrame';
複製程式碼

上面的方法應該就是排程螢幕重新整理的(如有錯誤,請指出,謝謝)。

到這裡,Ticker 也就 start 了,然後 Ticker 就會在動畫的每一幀去調一次自己的回撥,這個回撥 是在 AnimationController 中的構造方法中建立 Ticker 的時候傳入的 _ticker = vsync.createTicker(_tick);

我們來看看 _tick 部分原始碼:

void _tick(Duration elapsed) {
    
    ...
    
    notifyListeners();
    _checkStatusChanged();
  }
複製程式碼

這裡面呼叫了 notifyListeners()_checkStatusChanged(),我們先來看看 notifyListeners(),部分原始碼如下:

void notifyListeners() {
    final List<VoidCallback> localListeners = new List<VoidCallback>.from(_listeners);
    for (VoidCallback listener in localListeners) {
      try {
        if (_listeners.contains(listener))
          // 執行回撥
          listener();
      } catch (exception, stack) {
        ...
      }
    }
  }
複製程式碼

可以看出,notifyListeners() 中有一個 VoidCallback 的集合,然後遍歷這個集合,並且執行集合中所有的回撥。

通常我們會使用 .addListener((){...}) 給動畫新增監聽,我們在呼叫addListener((){...})之後會執行如下原始碼

void addListener(VoidCallback listener) {
    didRegisterListener();
    _listeners.add(listener);
  }
複製程式碼

將我們傳入的回撥方法新增到_listeners集合中,也就是上面 notifyListeners() 方法中遍歷的集合。

這裡我們來小結一下動畫的執行流程

  • 建立動畫
    • 建立 Ticker,並 傳入回撥,回撥裡面會執行 notifyListeners() 方法,此方法中會去遍歷_listeners集合,並執行集合中的每一個回撥方法
  • 給動畫新增監聽
    • 這裡監聽時傳入的回撥會被新增到 _listeners 集合中去
  • 啟動動畫
    • 會呼叫 ticker.srtart() 來啟動 Ticker,然後 Ticker 在動畫的每一幀都會 執行回撥,也就是我們在第一步中傳入的

接下來就是我們在 addListener((){...}) 中乾的事情了,通常會這樣

_controller.addListener(() {
      setState(() {

      });
    });
複製程式碼

也就是我們會在傳入的回撥方法中去執行 setState((){}) 方法,我們可以看看文件:

setState(VoidCallback fn) 部分文件如下:

Notify the framework that the internal state of this object has changed.

Calling [setState] notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a [build] for this [State] object.

大概意思就是:通知 framework 當前物件的內部狀態發生改變,這個操作會導致再次呼叫當前 State 物件的 build 方法,相當於重繪當前物件。

結合之前的分析,整個動畫過程就是,當動畫啟動之後,會不停的去執行addListener((){...})中的回撥,而我們在監聽回撥中又呼叫了setState(...)方法,所以這就導致當前物件不停的重繪,也就出現了螢幕上的動畫的效果。


回到之前的問題: 使用 addListener(...) 和 AnimatedWidget 為什麼會出現那種區別呢?

其實我們來看下 AnimatedWidget 的原始碼立馬就明白了

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    Key key,
    @required this.listenable
  }) : assert(listenable != null),
       super(key: key);
       
  final Listenable listenable;


  @protected
  Widget build(BuildContext context);

  @override
  _AnimatedState createState() => new _AnimatedState();
  
  ...
  
}
複製程式碼

我們可以看到 AnimatedWidget 繼承自 StatefulWidget, 而 StateFulWidget 在初始化的時候會去建立一個 State 物件來管理自身的狀態,也就是回去執行 createState() 方法。

其實到這裡已經可以解釋兩種動畫方式存在的區別,因為 AnimatedWidget 內部也有一個 State 物件來管理這自身的狀態,而我們之前通過檢視文件也知道一個 State 物件只會維護當前物件的狀態,所以即使重繪,也只會導致當前 State 物件的重繪,而不會導致其他 State 物件的重繪。

我們來繼續看看 AnimatedWidget 中的 createState() 幹了什麼事,它裡面呼叫了_AnimatedState() 方法,部分原始碼如下:

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

...

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
複製程式碼

明白了吧!!!

_AnimatedStateinitState 中給 widgetlistenable 新增了監聽,這裡的listenable 就是我們在初始化一個 AnimatedWidget 是傳入的 Animation 物件。

addListener 中傳入的回撥是 _handleChange(), 在 _handleChange() 中同樣呼叫了 setState(...)來觸發當前物件重繪。

好了, 整個過程就是這樣了。如果有什麼錯誤,歡迎大家指出,謝謝!!!

相關文章