首先,如果你還不知道什么是Flutter的話,請看這里,簡單講就是Google自己的React Native。它使用的編程語言是Dart,如果不知道什么是Dart的話請看這里。有人可能會問這兩個東西聽都沒聽過,學了有用嗎?我的答案是“俺也不知道”。
廢話都多說,下面開始學習Flutter的Animation。
Example
我們先來看下最后的運行結果:
基本邏輯就是點擊按鈕,執行一個Animation,然后我們的自定義View就會根據這個Animation不斷變化的值來繪制一個圓形。接下來就看看代碼是怎么實現的?
class _AnimationPainter extends CustomPainter{
Animation<double> _repaint ;
_AnimationPainter({
Animation<double> repaint
}):_repaint = repaint,super(repaint:repaint);
@override
void paint(Canvas canvas, Size size){
final Paint paint = new Paint()
..color = Colors.red[500].withOpacity(0.25)
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
canvas.drawCircle(new Point(size.width/2, size.height/2), _repaint.value, paint);
}
@override
bool shouldRepaint(_AnimationPainter oldDelegate){
return oldDelegate._repaint != _repaint;
}
}
首先我們先來自定義一個Painter,這個Painter繼承自CustomPainter
,構造方法中接收一個Animation對象。在它的paint()
方法中使用_repaint.value
作為圓形的半徑進行繪圖。
接下來我們再看主布局:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo')
),
body: new Center(
child: new CustomPaint(
key:new GlobalKey(),
foregroundPainter: new _AnimationPainter(
repaint:_animation
)
)
),
floatingActionButton: new FloatingActionButton(
onPressed: _startAnimation,
tooltip: 'startAnimation',
child: new Icon(Icons.add)
)
);
}
可以看到我們使用了一個CustomPaint
,在Flutter中這算是一個自定義布局。接著給它構造方法中的foregroundPainter
參數傳遞一個我們之前定義的_AnimationPainter
。
下面還定義了一個按鈕,點擊事件為_startAnimation
:
void _startAnimation() {
_animation.forward(from:0.0);
}
_startAnimation很簡單,就是用來開始一個動畫的。
OK,到這里布局就講完了,這時候我們還缺啥?對了,我們還缺一個Animation對象,下面就定義一個:
AnimationController _animation = new AnimationController(
duration: new Duration(seconds: 3),
lowerBound: 0.0,
upperBound: 500.0
);
大家可能會好奇AnimationController
又是個啥玩意?它其實是Animation的一個子類,可以用來控制Animation行為。
到這里所有相關代碼都交代完了,編譯運行就能出結果。
完整代碼:
import 'package:flutter/material.dart';
void main() {
runApp(
new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue
),
home: new FlutterDemo()
)
);
}
class FlutterDemo extends StatefulWidget {
FlutterDemo({Key key}) : super(key: key);
@override
_FlutterDemoState createState() => new _FlutterDemoState();
}
class _FlutterDemoState extends State<FlutterDemo> {
AnimationController _animation = new AnimationController(
duration: new Duration(seconds: 3),
lowerBound: 0.0,
upperBound: 500.0
);
void _startAnimation() {
_animation.forward(from:0.0);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo')
),
body: new Center(
child: new CustomPaint(
key:new GlobalKey(),
foregroundPainter: new _AnimationPainter(
repaint:_animation
)
)
),
floatingActionButton: new FloatingActionButton(
onPressed: _startAnimation,
tooltip: 'startAnimation',
child: new Icon(Icons.add)
)
);
}
}
class _AnimationPainter extends CustomPainter{
Animation<double> _repaint ;
_AnimationPainter({
Animation<double> repaint
}):_repaint = repaint,super(repaint:repaint);
@override
void paint(Canvas canvas, Size size){
final Paint paint = new Paint()
..color = Colors.red[500].withOpacity(0.25)
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
canvas.drawCircle(new Point(size.width/2, size.height/2), _repaint.value, paint);
}
@override
bool shouldRepaint(_AnimationPainter oldDelegate){
return oldDelegate._repaint != _repaint;
}
}
Animation原理
事物的運動狀態改變是需要外力的,Animation也一樣從靜止到運動(動畫執行階段),肯定有一股外力的存在,那這股力量就是啥?我們來一探究竟。
在Flutter中的一切都是由flutter engine來驅動的,看下圖:
由于flutter engine超出了我們討論范圍,我們這里只是假設它有一個叫做freshScreen()
的方法,用于刷新屏幕。對了,這里的freshScreen就是我們Animation最原始的動力,接下來介紹一個叫做
Scheduler
的類,如下圖:
它是一個單例,它內部有一個callback列表,用來存儲某些回調,而addFrameCallback方法就是往callback列表添加回調的。那這里的handleBeginFrame又是干么的呢?當flutter engine每調用一次freshScreen的時候,就回去調用Scheduler的handleBeginFrame方法,而在handleBeginFrame中會將callback列表中所有的回調調用一遍。并且會給每個回調傳遞一個當時的時間戳。到這里似乎還沒有Animation什么事,我們繼續往下看。
到這邊我們應該知道系統中有個叫Schedule的類,你只要調用它的addFrameCallback方法,向其中添加回調,那么這個回調就因為界面的刷新而一直被調用。那么又是誰來使用addFrameCallback呢?答案就是Ticker
。
Ticker是一個類似可控的計時器,調用它的start方法后,內部會調用它的_scheduleTick方法,其中會向Schedule中添加名為_tick的方法回調。而在_tick的方法回調中會調用到_onTick方法。這個_onTick方法是外部傳入的,可自定義其中的內容。并且每一次調用_onTick都會傳入一個時間參數,這個參數表示從調用start開始經歷的時間長度。
那么到這里我們知道了,我們不必直接跟Schedule打交道,有一個更好用的Ticker,我們只要給Ticker傳入一個回調,就能不斷的拿到一個△t,這個△t = now-timeStart。正是這個△t為我們推來了Animation的大門。
OK,到這里我們知道了Animation動畫執行的動力在哪了,接下來看看Animation內部是怎么利用Ticker實現的?
我們回到上面的小例子,拋開自定義的試圖,關鍵的代碼其實就兩行,如下:
AnimationController _animation = new AnimationController(
duration: new Duration(seconds: 3),
lowerBound: 0.0,
upperBound: 500.0
);
_animation.forward(from:0.0);
分別是初始化一個動畫和開始一個動畫。那就從這兩行入手,看看底下的源碼實現。
先看構造函數:
AnimationController({
double value,
this.duration,
this.debugLabel,
this.lowerBound: 0.0,
this.upperBound: 1.0
}) {
assert(upperBound >= lowerBound);
_direction = _AnimationDirection.forward;
_ticker = new Ticker(_tick);
_internalSetValue(value ?? lowerBound);
}
這里沒什么,只是初始化一些值,如動畫的值得上下邊界,執行時間和目前值。值得注意的是這里初始化了一個Ticker,我們看看Ticker的初始化都做了什么?
Ticker(TickerCallback onTick) : _onTick = onTick;
這里初始化了Ticker的_onTick方法,相當于傳入了一個回調用于Ticker來和Animation_controller交互。具體先不看傳入的這個_tick方法是啥,后面真正遇到了在做分析。
接著就是forward方法了,它的作用就是讓Animation的值從當前值變化到最大值。若不設置參數,即默認為下限值。
Future<Null> forward({ double from }) {
_direction = _AnimationDirection.forward;
if (from != null)
value = from;
return animateTo(upperBound);
}
OK,這是一個異步的方法,重點在animateTo這個方法,繼續:
Future<Null> animateTo(double target, { Duration duration, Curve curve: Curves.linear }) {
Duration simulationDuration = duration;
if (simulationDuration == null) {
assert(this.duration != null);
double range = upperBound - lowerBound;
double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
simulationDuration = this.duration * remainingFraction;
}
stop();
if (simulationDuration == Duration.ZERO) {
assert(value == target);
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
_checkStatusChanged();
return new Future<Null>.value();
}
assert(simulationDuration > Duration.ZERO);
assert(!isAnimating);
return _startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve));
}
雖然這里代碼有些多,但大部分是一些條件判斷和預處理,這里不需要理會,我們看關鍵的:
_startSimulation(new _InterpolationSimulation(_value, target, simulationDuration, curve));
這里引入了一個新的概念Simulation
,它其實相當于一個時間t和坐標x及速度v的關系,它有一個x(t)
方法,接受一個參數t及時間,返回t時間時x的值。在Animation中就是t時刻的值。而這個t就是Ticker產生的,我們接著往下看:
Future<Null> _startSimulation(Simulation simulation) {
assert(simulation != null);
assert(!isAnimating);
_simulation = simulation;
_lastElapsedDuration = Duration.ZERO;
_value = simulation.x(0.0).clamp(lowerBound, upperBound);
Future<Null> result = _ticker.start();
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward :
AnimationStatus.reverse;
_checkStatusChanged();
return result;
}
這里也是做了一些值得初始化,關鍵的一句是:
_ticker.start();
這里啟動了Ticker,我們看看start里面又做了什么?
Future<Null> start() {
assert(!isTicking);
assert(_startTime == null);
_completer = new Completer<Null>();
_scheduleTick();
if (SchedulerBinding.instance.isProducingFrame)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _completer.future;
}
上面我們主要關注 _scheduleTick()這個方法,
void _scheduleTick({ bool rescheduling: false }) {
assert(isTicking);
assert(_animationId == null);
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
到這里小伙伴們應該已經看出來了,這里Ticker將自己的_tick方法回調注冊到了Scheduler中,一旦注冊完,隨著屏幕的刷新,_tick將會被不停的調用,
void _tick(Duration timeStamp) {
assert(isTicking);
assert(_animationId != null);
_animationId = null;
if (_startTime == null)
_startTime = timeStamp;
_onTick(timeStamp - _startTime);
// The onTick callback may have scheduled another tick already.
if (isTicking && _animationId == null)
_scheduleTick(rescheduling: true);
}
_tick方法中主要做了對時間的處理,它會將從開始到當前的時間間隔傳給_onTick這個方法,而這個方法就是之前AnimationController傳遞進來的。及下面的_tick方法:
void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.MICROSECONDS_PER_SECOND;
_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
stop();
}
notifyListeners();
_checkStatusChanged();
}
這里應該看的很明白了,AnimationController的_tick會被不停的調用,而AnimationController的值則是由_simulation來根據時間計算得來。接著再調用notifyListeners()和 _checkStatusChanged()通知監聽AnimatinController的對象。
Animation的運行原理差不多就是這些,有疑問或對此感興的可以在我主頁中找到我的微信二維碼,加我好友,我們慢慢聊。