Flutter的動畫大致能夠分爲使用AnimationController 和Animation控制的基礎動畫、使用 Hero的轉場動畫和使用CustomPainter 的自定義動畫三大類。除此以外,Flutter還支持矢量動畫,是一種相似Android開發中的Lottie動畫。git
Flare是一家能夠快速製做矢量動畫的網站,提供專門的Flutter組件來承載網站導出的動畫文件,使用Flare建立的動畫不只能夠有效減小安裝包的體積,還能建立更加複雜絢麗的動畫體驗。Flare動畫最先出如今2019年12月舉行的Flutter技術大會上,一經發布立馬受到開發者的喜好和追捧。github
做爲一個專業製做矢量動畫的網站,Flare提供了很是豐富的免費矢量動畫。因爲Flare並無提供桌面版的開發工具,因此建立Flare動畫以前須要登陸Flare官網來製做Flare動畫文件,若是尚未Flare帳號能夠先註冊一個。segmentfault
Flare一般以工程形式來建立和管理動畫項目,目前Flare支持建立動畫項目有兩類,分別是Flare和Nima,它們的區別以下。windows
因爲Nima主要用於構建2D遊戲動畫,因此若是是普通的應用開發只須要新建一個Flare項目便可。打開Flare官網,而後點擊【Your Files】菜單便可新建一個Flare項目,以下圖所示。app
而後,系統會初始化一個空白的工做區用於開發者建立和製做動畫文件,以下圖所示。
在工做區的左上角有兩個切換按鈕,分別是SETUP和ANIMATE,表示兩種不一樣的工做模式。其中,SETUP模式用於導入和繪製矢量元素,而ANIMATE模式則用於處理矢量元素的動畫交互,動畫交互須要用到的動畫節點名稱位於工做區的左下角。ide
一般,製做Flare動畫文件是一項專業且複雜的工做,若是隻是爲了體驗Flare動畫的魅力,那麼可使用Flare提供的免費矢量動畫,以下圖所示。
工具
若是咱們須要建立Flare動畫,那麼首先須要初始化一個動畫項目,以下圖所示。
如上圖所示,在工做區的左上角有兩個切換按鈕,分別是SETUP和ANIMATE,表示兩種不一樣的工做模式。post
在SETUP模式下,咱們能夠經過Hierarchy樹狀圖來查看全部控件的層級結構關係,通常頂級結點是一個artboard,能夠定義scene的尺寸、背景顏色等屬性。一個Flare動畫能夠有多個artboard,而且控件均可以擁有本身的子控件,子控件會繼承父控件的全部變換。若是要添加矢量元素,能夠點擊 SETUP模式下工做區的「+」號按鈕,以下圖所示。
咱們以製做一個按鈕爲例。首先,咱們選擇菜單中的矩形,而後選中矩形,右側會出現屬性菜單欄,能夠修改位置、大小、顏色、線條等等屬性,以下圖所示。
固然,咱們也能夠按住鼠標右鍵(或者按住空格拖動鼠標)能夠拖動畫布,滾輪放大/縮小,上下左右鍵精確調整位置,Shift+上下左右鍵能夠大幅調整位置。接下來,咱們切換到ANIMATE模式添加動畫,底下會多出一行動畫控制面板,以下圖。
首先,打開動畫時長區間,將指針撥到00:01:00(mac可使用快捷鍵command+shift+左右,windows可使用快捷鍵ctrl+shift+左右,一次調整10幀),並在在00:01:00處更改矩形的屬性,以下圖所示。
而後,點擊左下角的播放鍵,效果以下圖。
最後,將製做好的Flare動畫文件導出便可。
關於如何建立Flare動畫,能夠參考官方開源的例子,以及 Flare動畫和Flutter動畫之Flare的製做與使用。開發工具
製做Flare動畫文件是一項專業且複雜的工做,若是隻是爲了體驗Flare動畫的魅力,那麼可使用Flare提供的免費矢量動畫。首先,打開一個免費的矢量動畫,而後點擊面板中【OPEN IN RIVE】按鈕打開Flare動畫文件,以下圖所示。
而後,點擊工做區右上角的導出圖標便可導出Flare動畫文件,該文件是一個flr 格式的文件,Flare動畫組件操做的就是該文件。動畫
在Flutter中開發Flare動畫須要使用到flare_flutter
或者smart_flare
庫。其中,smart_flare庫是對flare_flutter庫的高度封裝,開發者只須要使用少許代碼便可實現與Flare動畫的交互。打開Flutter工程,並在pubspec.yaml文件中添加以下依賴配置。
dependencies: flare_flutter: ^2.0.5 smart_flare: ^0.2.9+1
而後,使用flutter packages get命令將依賴的插件拉取到本地。而後,將以前導出的flr動畫文件拷貝到assets資源目錄下,並在pubspec.yaml配置文件中註冊該動畫文件,以下所示。
assets: - assets/button-animation.flr
若是隻是單純的加載動畫文件,而不須要處理與動畫交互,那麼可使用flare_flutter庫提供的FlareActor組件來加載動畫文件,以下所示。
FlareActor( "assets/Shake.flr", animation: "idle", alignment: Alignment.center, fit: BoxFit.contain )
其中,Shake.flr表示Flare動畫文件的名稱,animation表示動畫的初始節點。一般,flr文件會有多個動畫節點,可使用artboard.getNode(String name)方法獲取動畫的節點,而後經過節點來對動畫進行精確地控制。
使用flare_flutter庫執行動畫交互操做時,須要咱們繼承FlareControls類,並對initialize()、advance()和setViewTransform()三個方法進行重寫,以下所示。
例如,下面是使用flare_flutter
庫實現登陸的動畫,在此登陸交互動畫中,效果以下圖。
在上面的動畫交互中,主要包含以下6種動畫交互事件,分別是:
那麼若是要對用戶的行爲進行精準的響應,那麼就須要咱們繼承FlareControls,而後經過ActorNode的artboard.getNode(String name)
獲取節點後執行對於的事件,以下所示。
class FlareSignInController extends FlareControls { ActorNode _faceControl; Mat2D _globalToFlareWorld = Mat2D(); Vec2D _caretGlobal = Vec2D(); Vec2D _caretWorld = Vec2D(); Vec2D _faceOrigin = Vec2D(); Vec2D _faceOriginLocal = Vec2D(); bool _hasFocus = false; String _password; static const double _projectGaze = 60.0; @override bool advance(FlutterActorArtboard artboard, double elapsed) { super.advance(artboard, elapsed); Vec2D targetTranslation; if (_hasFocus) { Vec2D.transformMat2(_caretWorld, _caretGlobal, _globalToFlareWorld); _caretWorld[1] += sin(new DateTime.now().millisecondsSinceEpoch / 300.0) * 70.0; Vec2D toCaret = Vec2D.subtract(Vec2D(), _caretWorld, _faceOrigin); Vec2D.normalize(toCaret, toCaret); Vec2D.scale(toCaret, toCaret, _projectGaze); Mat2D toFaceTransform = Mat2D(); if (Mat2D.invert(toFaceTransform, _faceControl.parent.worldTransform)) { Vec2D.transformMat2(toCaret, toCaret, toFaceTransform); targetTranslation = Vec2D.add(Vec2D(), toCaret, _faceOriginLocal); } } else { targetTranslation = Vec2D.clone(_faceOriginLocal); } Vec2D diff = Vec2D.subtract(Vec2D(), targetTranslation, _faceControl.translation); Vec2D frameTranslation = Vec2D.add(Vec2D(), _faceControl.translation, Vec2D.scale(diff, diff, min(1.0, elapsed * 5.0))); _faceControl.translation = frameTranslation; return true; } @override void initialize(FlutterActorArtboard artboard) { super.initialize(artboard); _faceControl = artboard.getNode("ctrl_face"); if (_faceControl != null) { _faceControl.getWorldTranslation(_faceOrigin); Vec2D.copy(_faceOriginLocal, _faceControl.translation); } play("idle"); } @override void onCompleted(String name) { play("idle"); } @override void setViewTransform(Mat2D viewTransform) { Mat2D.invert(_globalToFlareWorld, viewTransform); } void lookAt(Offset caret) { if (caret == null) { _hasFocus = false; return; } _caretGlobal[0] = caret.dx; _caretGlobal[1] = caret.dy; _hasFocus = true; } void setPassword(String value) { _password = value; } bool _isCoveringEyes = false; coverEyes(cover) { if (_isCoveringEyes == cover) { return; } _isCoveringEyes = cover; if (cover) { play("hands_up"); } else { play("hands_down"); } } void submitPassword() { if (_password == "bears") { play("success"); } else { play("fail"); } } }
因爲使用flare_flutter
庫實現登陸動畫比較複雜,因此詳細的代碼就很少講解,有興趣的能夠看看源碼:登陸動畫源碼。
使用flare_flutter實現的Flare動畫,須要開發者編寫FlareControls來控制動畫交互,須要開發者具有較好的數學和物理基礎,實現起來也比較複雜。相比flare_flutter插件庫來講,使用smart_flare插件庫實現Flare動畫要簡單許多,只須要調用smart_flare插件庫提供的組件,而後傳入對應的參數便可。
ActiveArea( debugArea: true, area: Rect.fromLTWH(thirdOfWidth*2, 0, thirdOfWidth, animationHeight / 2), animationName: 'image_tapped', onAreaTapped: () { print('image_tapped…'); } ),
其中,area表示須要顯示的元素在屏幕的位置,animationName表示執行動畫交互時動畫節點的名稱,debugArea表示是否開啓調試模式,若是開啓調試模式會看到該元素區域有一個陰影,onAreaTapped用於響應用戶的點擊。
例如,下面是使用smart_flare庫提供的SmartFlareActor和ActiveArea組件實現的菜單動畫的例子,效果以下。
下面是smart_flare庫實現按鈕彈出菜單的示例,源碼以下。
class FlareAnimPage extends StatefulWidget { @override _FlareAnimPageState createState() => _FlareAnimPageState(); } class _FlareAnimPageState extends State<FlareAnimPage> { @override Widget build(BuildContext context) { var animW = 295.0; var animH = 251.0; var animWThirds = animW / 3; var halfAnimHeight = animH / 2; var activeAreas = [ ActiveArea( area: Rect.fromLTWH(0, 0, animWThirds, halfAnimHeight), debugArea: false, guardComingFrom: ['deactivate'], animationName: 'camera_tapped', ), ActiveArea( area: Rect.fromLTWH(animWThirds, 0, animWThirds, halfAnimHeight), debugArea: false, guardComingFrom: ['deactivate'], animationName: 'pulse_tapped'), ActiveArea( area: Rect.fromLTWH(animWThirds * 2, 0, animWThirds, halfAnimHeight), debugArea: false, guardComingFrom: ['deactivate'], animationName: 'image_tapped', onAreaTapped: () { print('image_tapped!'); } ), ActiveArea( area: Rect.fromLTWH(0, animH / 2, animW, animH / 2), debugArea: true, animationsToCycle: ['activate', 'deactivate'], onAreaTapped: () { print('Button tapped!'); }) ]; return Scaffold( appBar: AppBar( title: Text('Flare Anim'), ), body: Container( color: Color(0xffcccccc), child: Align( alignment: Alignment.bottomCenter, child: SmartFlareActor( width: animW, height: animH, filename: 'assets/button-animation.flr', startingAnimation: 'deactivate', activeAreas: activeAreas, ), ), ), ); } }