Analysis of Flutter Framework

Analysis of the Flutter Framework (I) — Overview and Window

Analysis of the Flutter Framework (II) — Initialization

Analysis of the Flutter Framework (iii) — Widget, Element and RenderObject

Analysis of The Flutter Framework (IV) — The Operation of the Flutter Framework

Analysis of the Flutter Framework (5) — Animation

Analysis of the Flutter Framework (VI) — Layout

Analysis of the Flutter Framework (VII) — Drawing

preface

The first four articles introduced the whole picture of the Flutter framework, and I believe you have a general understanding of the Flutter framework. This series of articles has always revolved around the various stages of the rendering pipeline. We know that the Animate phase is the first to run after the Vsync signal arrives. This phase starts when engine calls back to the window’s onBeginFrame function. In this article we will introduce the basic principles of Flutter animation.

example

The so-called animation is actually a series of continuously changing pictures in a very short time frame by frame display, the human eye is animation. Here is a simple example of how to run an animation in Flutter:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: LogoAnim()));
}

class LogoAnim extends StatefulWidget {
  _LogoAnimState createState() => _LogoAnimState();
}

class _LogoAnimState extends State<LogoAnim> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller) .. addListener(() { setState(() { }); }); controller.forward(from:0);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  void dispose() {
    controller.dispose();
    super.dispose(); }}Copy the code

This animation shows a Flutter logo on the mobile phone screen in gradual progression from small to large. From the above code we can see that implementing an animation in Flutter does several things.

  1. The first one to animateWidgetIs aStatefulWidget. itsStateTo mix (mixin) SingleTickerProviderStateMixin.
  2. ininitState()To add animation-related initializations, here we instantiate two classesAnimationControllerandAnimation. instantiationAnimationControllerWe pass in two parameters, one is the duration of the animation, the other isStateThemselves, here is actually used to mixSingleTickerProviderStateMixin. Instantiate another oneAnimationThe first thing we instantiate is aTween. This class actually represents a linear change from minimum to maximum. So when you instantiate you pass in the start and end values. And then callanimate()And pass in the previous onecontroller. This call will return what we needAnimationInstance. Obviously we need to know the message when the properties of the animation change, so this will pass..addListener()toAnimationInstance registration callback. This callback does only one thing, and that is to callsetState()To update the UI. And then finally, callcontroller.forward()To start the animation.
  3. Note that inbuild()In the function we buildwidgetIt’s time to use itanimation.value. So the chain here is the animation that gets called when it gets a callbacksetState()From our last articlesetStateThis is followed by the construction phase of the rendering pipelinebuild()To rebuildWidget. When they rebuilt it, they used it after the changeanimation.value. This frame by frame loop, our animation is moving.
  4. Finally, indispose()Remember to callcontroller.dispose()Release resources.

Let’s dive into the Flutter source to see how the animation works.

Analysis of the

First of all, we look at the SingleTickerProviderStateMixin of blended into the State.

mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
  Ticker _ticker;

  @override
  Ticker createTicker(TickerCallback onTick) {
    _ticker = Ticker(onTick, debugLabel: 'created by $this');
    return _ticker;
  }

  @override
  void didChangeDependencies() {
    if(_ticker ! = null) _ticker.muted = ! TickerMode.of(context); super.didChangeDependencies(); }}Copy the code

The only thing this blend does is implement createTicker() to instantiate a Ticker class. In another function, didChangeDependencies(), there is a line _ticker.fraternal =! TickerMode.of(context); . This line of code means whether to mute your _ticker when the animated State’s dependency in the Element Tree changes. One scenario is that the animation on the current page is still playing and the user navigates to another page. The animation on the current page is not necessary to play, whereas the animation may continue to play when the page is switched back. The control is here, note the tickermode.of (context). We see this in a lot of places in the Flutter framework, which is basically a way to find the corresponding InheritedWidget from the Element Tree’s ancestors.

The Ticker, as the name suggests, provides vsync signals for animations. Let’s take a look at the source code to find out.

class Ticker {

  TickerFuture _future;
  
  bool get muted => _muted;
  bool _muted = false;
  set muted(bool value) {
    if (value == muted)
      return;
    _muted = value;
    if (value) {
      unscheduleTick();
    } else if(shouldScheduleTick) { scheduleTick(); }}bool get isTicking {
    if (_future == null)
      return false;
    if (muted)
      return false;
    if (SchedulerBinding.instance.framesEnabled)
      return true;
    if(SchedulerBinding.instance.schedulerPhase ! = SchedulerPhase.idle)return true; 
    return false;
  }

  bool getisActive => _future ! =null;

  Duration _startTime;

  TickerFuture start() {
    _future = TickerFuture._();
    if (shouldScheduleTick) {
      scheduleTick();
    }
    if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
        SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
      _startTime = SchedulerBinding.instance.currentFrameTimeStamp;
    return _future;
  }
  
  void stop({ bool canceled = false{})if(! isActive)return;

    final TickerFuture localFuture = _future;
    _future = null;
    _startTime = null;

    unscheduleTick();
    if (canceled) {
      localFuture._cancel(this);
    } else{ localFuture._complete(); }}final TickerCallback _onTick;

  int _animationId;

  @protected
  bool getscheduled => _animationId ! =null;

  @protected
  bool getshouldScheduleTick => ! muted && isActive && ! scheduled;void _tick(Duration timeStamp) {
    _animationId = null; _startTime ?? = timeStamp; _onTick(timeStamp - _startTime);if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  }

  @protected
  void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }

  @protected
  void unscheduleTick() {
    if (scheduled) {
      SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId);
      _animationId = null; }}}Copy the code

As you can see, what the Ticker is mainly doing is a bit like controlling a timer, with start() and stop() and mute. Also record your current state isTicking. The scheduleTick() function is the one we need to focus on:

@protected
  void scheduleTick({ bool rescheduling = false }) {
    _animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
  }
Copy the code

As you can see, this is where the SchedulerBinding comes in. The Ticker callback function _tick is passed in during scheduling.

  int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
    scheduleFrame();
    _nextFrameCallbackId += 1;
    _transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
    return _nextFrameCallbackId;
  }
Copy the code

The _tick Ticker callback function is added to transientCallbacks when a frame is scheduled. From our previous analysis of the rendering pipeline, we know that the transientCallbacks are executed once in the Window onBeginFrame callback after the vsync signal. At this point, you enter the Animate stage of the rendering pipeline.

Now let’s look at what the Ticker callback _tick does:

  void _tick(Duration timeStamp) {
    _animationId = null; _startTime ?? = timeStamp; _onTick(timeStamp - _startTime);if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  }
Copy the code

Here the _onTick is passed in when the Ticker is instantiated. After _onTick is called, the Ticker will schedule a new frame if it finds that its task has not been completed and continues to tick. So what motivates you to watch the animation is actually the Vsync signal.

So what does this _onTick look like? This function is passed in when the Ticker is instantiated. And we know from the above analysis, the Ticker is instantiated in the calling TickerProvider. CreateTicker () when done. Who calls this function? Is AnimationController.

  AnimationController({
    double value,
    this.duration,
    this.debugLabel,
    this.lowerBound = 0.0.this.upperBound = 1.0.this.animationBehavior = AnimationBehavior.normal,
    @required TickerProvider vsync,
  }) : _direction = _AnimationDirection.forward {
    _ticker = vsync.createTicker(_tick);
    _internalSetValue(value ?? lowerBound);
  }
Copy the code

As you can see, createTicker() is called in the constructor, passing in _ticker. Now, _ticker.

  void _tick(Duration elapsed) {
    _lastElapsedDuration = elapsed;
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    _value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    if (_simulation.isDone(elapsedInSeconds)) {
      _status = (_direction == _AnimationDirection.forward) ?
        AnimationStatus.completed :
        AnimationStatus.dismissed;
      stop(canceled: false);
    }
    notifyListeners();
    _checkStatusChanged();
  }
Copy the code

The callback does these things and updates the new values based on the timestamp after vsync arrives. In this case, _simulation is used. Why is it called that? Because this is used to simulate an object under the action of external forces at different points in the state of motion changes, this is also the essence of animation.

The new values are called notifyListeners() to notify the observers. Remember from the first example that we instantiated the animation and then passed the.. AddListener () added callback? This is where the callback is going to be called, setState() is going to be called. Next comes the build phase of the rendering pipeline.

You might be wondering, what is the Tween in that example for when the AnimationController does that?

As we can see from the constructor of the AnimationController, it only simulates between [0.0, 1.0], that is, it outputs values between 0.0 and 1.0 at any one time regardless of how the animation is moving, but our animations have rotation angles, color gradients, graphic changes, and more complex combinations. Obviously, we need to find a way to convert values between 0.0 and 1.0 to the Angle, position, color, opacity, etc. This transformation is done by various Animation, such as Tween, its task during the Animation from 0 to 300. So how do we do that? After instantiating Tween we call animate(), passing in an AnimationController instance.

  Animation<T> animate(Animation<double> parent) {
    return _AnimatedEvaluation<T>(parent, this);
  }

Copy the code

You see, the input parameter is an Animation

, which in this case is the AnimationController. The output parameter is an Animation

. This completes the change from [0.0, 1.0] to any type.

How do you do that? This change actually happens when the value is used. In the example above, the widget is built in the state.build () function and the getter is called to animation.value. This actually calls _animatedeval. Value.

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

_evaluatable is Tween and parent is AnimationController. So, Tween does the conversion itself, and only Tween knows what output it needs.

T evaluate(Animation<double> animation) => transform(animation.value);
Copy the code

We’re in transform() again

@override
  T transform(double t) {
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return lerp(t);
  }
Copy the code

See the scope limit? The real transformation is done in lerp() again.

@protected
  T lerp(double t) {
    return begin + (end - begin) * t;
  }
Copy the code

Very simple linear interpolation.

So you can understand what the Tween animation in Flutter is doing by knowing what it’s doing inside its transform() function. It’s just a linear interpolation animation. Tween is linear interpolation, but what if I want to do nonlinear interpolation animation? Use CurvedAnimation. Flutter has a large list of all kinds of linear and nonlinear interpolating animations. You can even define your own nonlinear animations by rewriting the transform function:

import 'dart:math';
class ShakeCurve extends Curve {
  @override
  double transform(double t) => sin(t * pi * 2);
}
Copy the code

Ok, so much for the animation in the Flutter framework.

conclusion

This article is the fifth in a series of articles on Flutter framework analysis, focusing on the rendering pipeline of Flutter as a clue to its operation. This paper mainly aims at the animation stage of the rendering pipeline, and makes a brief analysis of Flutter animation mechanism from the underlying perspective. You are expected to have a basic understanding of Flutter animation. A dizzying array of animation-related widgets have been spawned from this, with the tao begetting one, the life begetting two, the two begetting three, the three begetting everything. If you master the Tao, you will not be deceived by everything.