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.
- The first one to animate
Widget
Is aStatefulWidget
. itsState
To mix (mixin
)SingleTickerProviderStateMixin
. - in
initState()
To add animation-related initializations, here we instantiate two classesAnimationController
andAnimation
. instantiationAnimationController
We pass in two parameters, one is the duration of the animation, the other isState
Themselves, here is actually used to mixSingleTickerProviderStateMixin
. Instantiate another oneAnimation
The 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 needAnimation
Instance. Obviously we need to know the message when the properties of the animation change, so this will pass..addListener()
toAnimation
Instance 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. - Note that in
build()
In the function we buildwidget
It’s time to use itanimation.value
. So the chain here is the animation that gets called when it gets a callbacksetState()
From our last articlesetState
This 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. - Finally, in
dispose()
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.