文章地址:mrw.so/4VPxov 譯者:依然範特稀西html
在本文中,咱們將經過在Flutter中建立一個拍手動畫的模型,來學習一些有關動畫的核心概念。git
就像標題中所說的那樣,本文將更多地關注動畫,而不會關注Flutter的基礎知識。github
咱們將從建立一個新的Flutter項目生成的代碼開始,建立一個新的Flutter項目,你就會獲得下面的代碼:api
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}
複製代碼
Flutter爲咱們提供了一些免費的入門代碼,它爲咱們建立了一個浮動操做按鈕,而且自動幫咱們管理計數的狀態。數組
下圖是咱們最終要實現的效果:app
在添加動畫以前,讓咱們快速解決一些簡單的問題:less
更改按鈕圖標和背景。dom
按住按鈕時,按鈕應繼續增長計數。ide
讓咱們快速解決上面2個問題,而後開始實現動畫:函數
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
final duration = new Duration(milliseconds: 300);
Timer timer;
initState() {
super.initState();
}
dispose() {
super.dispose();
}
void increment(Timer t) {
setState(() {
_counter++;
});
}
void onTapDown(TapDownDetails tap) {
// User pressed the button. This can be a tap or a hold.
increment(null); // Take care of tap
timer = new Timer.periodic(duration, increment); // Takes care of hold
}
void onTapUp(TapUpDetails tap) {
// User removed his finger from button.
timer.cancel();
}
Widget getScoreButton() {
return new Positioned(
child: new Opacity(opacity: 1.0, child: new Container(
height: 50.0 ,
width: 50.0 ,
decoration: new ShapeDecoration(
shape: new CircleBorder(
side: BorderSide.none
),
color: Colors.pink,
),
child: new Center(child:
new Text("+" + _counter.toString(),
style: new TextStyle(color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15.0),))
)),
bottom: 100.0
);
}
Widget getClapButton() {
// Using custom gesture detector because we want to keep increasing the claps
// when user holds the button.
return new GestureDetector(
onTapUp: onTapUp,
onTapDown: onTapDown,
child: new Container(
height: 60.0 ,
width: 60.0 ,
padding: new EdgeInsets.all(10.0),
decoration: new BoxDecoration(
border: new Border.all(color: Colors.pink, width: 1.0),
borderRadius: new BorderRadius.circular(50.0),
color: Colors.white,
boxShadow: [
new BoxShadow(color: Colors.pink, blurRadius: 8.0)
]
),
child: new ImageIcon(
new AssetImage("images/clap.png"), color: Colors.pink,
size: 40.0),
)
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme
.of(context)
.textTheme
.display1,
),
],
),
),
floatingActionButton: new Padding(
padding: new EdgeInsets.only(right: 20.0),
child: new Stack(
alignment: FractionalOffset.center,
overflow: Overflow.visible,
children: <Widget>[
getScoreButton(),
getClapButton(),
],
)
),
);
}
}
複製代碼
看了上面最終的效果圖,咱們須要作2件事:
widgets
的大小。widget
,釋放按鈕時將其隱藏。widget
併爲其設置動畫。讓咱們一個接一個地慢慢增長學習曲線。首先,咱們須要瞭解有關Flutter動畫的一些基本知識。
動畫不過是隨着時間變化的一些值,例如,當咱們點擊按鈕時,咱們但願用動畫來讓顯示分數widget
從底部升起,而當手指離開按鈕時,繼續上升而後隱藏。
若是僅看分數Widget
,咱們須要在一段時間內更改Widget
的位置和不透明度值。
new Positioned(
child: new Opacity(opacity: 1.0,
child: new Container(
...
)),
bottom: 100.0
);
複製代碼
假設咱們但願分數widget須要150毫秒才能從底部顯示出來。在如下時間軸上考慮一下:
這是一個簡單的2D圖形。 position
將隨着時間而改變。 請注意,對角線是直線。若是你喜歡,它也能夠是曲線。
你可使position
隨時間緩慢增長,而後變得愈來愈快。或者,你也可讓它以超高速進入,而後在最後放慢速度。
下面是咱們介紹的第一個組件:Animation Controller
。
scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
複製代碼
在這裏,咱們爲動畫建立了一個簡單的控制器(Controller
)。咱們已經指定但願動畫運行150ms
。可是,vsync
是什麼東西?
移動設備每隔幾毫秒刷新一次屏幕。這就是咱們將一組圖像視爲連續流或電影的方式。
屏幕刷新的速率因設備而異。 假設移動設備每秒刷新屏幕60次(每秒60幀)。 那就是每16.67毫秒以後,咱們就會向大腦提供新的圖像。 有時,圖像就會錯位(在屏幕刷新時發出不一樣的圖像),而且看到屏幕撕裂。 VSync
就是解決這個問題的。
咱們給控制器
設置一個監聽器,而後開始動畫:
scoreInAnimationController.addListener(() {
print(scoreInAnimationController.value);
});
scoreInAnimationController.forward(from: 0.0);
/* OUTPUT I/flutter ( 1913): 0.0 I/flutter ( 1913): 0.0 I/flutter ( 1913): 0.22297333333333333 I/flutter ( 1913): 0.3344533333333333 I/flutter ( 1913): 0.4459333333333334 I/flutter ( 1913): 0.5574133333333334 I/flutter ( 1913): 0.6688933333333335 I/flutter ( 1913): 0.7803666666666668 I/flutter ( 1913): 0.8918466666666668 I/flutter ( 1913): 1.0 */
複製代碼
控制器在150ms
內生成了0.0
到1.0
的數字。請注意,生成的值幾乎是線性的。 0.2
、0.3
、0.4
…咱們如何改變這種行爲?這將在第二部分完成:曲線動畫
。
bounceInAnimation = new CurvedAnimation(parent: scoreInAnimationController, curve: Curves.bounceIn);
bounceInAnimation.addListener(() {
print(bounceInAnimation.value);
});
/*OUTPUT I/flutter ( 5221): 0.0 I/flutter ( 5221): 0.0 I/flutter ( 5221): 0.24945376519722218 I/flutter ( 5221): 0.16975716286388898 I/flutter ( 5221): 0.17177866222222238 I/flutter ( 5221): 0.6359024059750003 I/flutter ( 5221): 0.9119433941222221 I/flutter ( 5221): 1.0 */
複製代碼
經過將parent
屬性設置爲咱們的控制器,並提供動畫遵循曲線,就能夠建立一個CurvedAnimation
,Flutter曲線文檔頁面上提供了多種曲線供咱們選擇:api.flutter.dev/flutter/ani…
控制器在150ms
的時間內爲曲線動畫Widget
提供從0.0
到1.0
的值。曲線動畫Widget
根據咱們設置的曲線對這些值進行插值。
儘管咱們獲得了0.0
到1.0
之間的一系列值,可是咱們但願顯示分數的Widget
顯示的值爲0-100
,咱們能夠簡單地乘以100來獲得結果,或者咱們可使用第三個組件:Tween
類。
tweenAnimation = new Tween(begin: 0.0, end: 100.0).animate(scoreInAnimationController);
tweenAnimation.addListener(() {
print(tweenAnimation.value);
});
/* Output I/flutter ( 2639): 0.0 I/flutter ( 2639): 0.0 I/flutter ( 2639): 33.452000000000005 I/flutter ( 2639): 44.602000000000004 I/flutter ( 2639): 55.75133333333334 I/flutter ( 2639): 66.90133333333334 I/flutter ( 2639): 78.05133333333333 I/flutter ( 2639): 89.20066666666668 I/flutter ( 2639): 100.0 */
複製代碼
Tween
類生成begin
到end
之間的值,前面咱們已經使用過線性的scoreInAnimationController
,相反,咱們可使用反彈曲線來得到不一樣的值。Tween
的優勢遠不止這些,你還能夠補間其餘東西,好比你能夠補間color(顏色)
、offset(偏移量)
、position(位置)
、和其餘Widget屬性,從而進一步擴展了基礎補間類。
至此,咱們已經掌握了足夠的知識,如今可使咱們的得分Widget
在按下按鈕時從底部彈出,而在離開時隱藏。
initState() {
super.initState();
scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
scoreInAnimationController.addListener((){
setState(() {}); // Calls render function
});
}
void onTapDown(TapDownDetails tap) {
scoreInAnimationController.forward(from: 0.0);
...
}
Widget getScoreButton() {
var scorePosition = scoreInAnimationController.value * 100;
var scoreOpacity = scoreInAnimationController.value;
return new Positioned(
child: new Opacity(opacity: scoreOpacity,
child: new Container(...)
),
bottom: scorePosition
);
}
複製代碼
如上圖所示,點擊按鈕,Score Widget
從底部彈出了,可是這兒還有一個小問題:當屢次點擊按鈕的時候,score widget
一次又一次的彈出,這是因爲上述代碼中的一個小錯誤。每次點擊按鈕時,咱們都告訴控制器從0開始,即forward(from: 0.0)
。
如今,咱們爲score Widget
添加退出動畫,首先,咱們添加一個枚舉來更輕鬆地管理score Widget
的狀態。
enum ScoreWidgetStatus {
HIDDEN,
BECOMING_VISIBLE,
BECOMING_INVISIBLE
}
複製代碼
而後,建立一個退出動畫的控制器,動畫控制器將使score widget
的位置從100
非線性變化到150
。咱們還爲動畫添加了狀態監聽器。動畫結束後,咱們將得分組件
的狀態設置爲隱藏。
scoreOutAnimationController = new AnimationController(vsync: this, duration: duration);
scoreOutPositionAnimation = new Tween(begin: 100.0, end: 150.0).animate(
new CurvedAnimation(parent: scoreOutAnimationController, curve: Curves.easeOut)
);
scoreOutPositionAnimation.addListener((){
setState(() {});
});
scoreOutAnimationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_scoreWidgetStatus = ScoreWidgetStatus.HIDDEN;
}
});
複製代碼
當用戶手指離開組件的時候,咱們將相應地設置狀態,並啓動300毫秒
的計時器。 300毫秒
後,咱們將爲得分組件
添加位置和不透明度動畫。
void onTapUp(TapUpDetails tap) {
// User removed his finger from button.
scoreOutETA = new Timer(duration, () {
scoreOutAnimationController.forward(from: 0.0);
_scoreWidgetStatus = ScoreWidgetStatus.BECOMING_INVISIBLE;
});
holdTimer.cancel();
}
複製代碼
咱們還修改了onTapDown
事件以處理某些邊角狀況。
void onTapDown(TapDownDetails tap) {
// User pressed the button. This can be a tap or a hold.
if (scoreOutETA != null) scoreOutETA.cancel(); // We do not want the score to vanish!
if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN) {
scoreInAnimationController.forward(from: 0.0);
_scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;
}
increment(null); // Take care of tap
holdTimer = new Timer.periodic(duration, increment); // Takes care of hold
}
複製代碼
最後,咱們須要選擇用於score widget
的位置和不透明度的控制器值。一個簡單的開關就完成了。
Widget getScoreButton() {
var scorePosition = 0.0;
var scoreOpacity = 0.0;
switch(_scoreWidgetStatus) {
case ScoreWidgetStatus.HIDDEN:
break;
case ScoreWidgetStatus.BECOMING_VISIBLE :
scorePosition = scoreInAnimationController.value * 100;
scoreOpacity = scoreInAnimationController.value;
break;
case ScoreWidgetStatus.BECOMING_INVISIBLE:
scorePosition = scoreOutPositionAnimation.value;
scoreOpacity = 1.0 - scoreOutAnimationController.value;
}
return ...
}
複製代碼
score widget
的運行效果很棒,先彈出而後逐漸消失。
到這一步,咱們幾乎知道如何在分數增長時也改變大小。讓咱們快速添加大小動畫,而後繼續搞火花閃爍效果
。
我已經更新了ScoreWidgetStatus
枚舉來保留一個額外的VISIBLE
值。如今,咱們爲size
屬性添加一個新的控制器。
scoreSizeAnimationController = new AnimationController(vsync: this, duration: new Duration(milliseconds: 150));
scoreSizeAnimationController.addStatusListener((status) {
if(status == AnimationStatus.completed) {
scoreSizeAnimationController.reverse();
}
});
scoreSizeAnimationController.addListener((){
setState(() {});
});
複製代碼
控制器在150ms
的時間內生成從0
到1
的值,完成以後((status == AnimationStatus.completed
),又會生成從1
到0
的值。這會產生很好的增加和收縮效果。
void increment(Timer t) {
scoreSizeAnimationController.forward(from: 0.0);
setState(() {
_counter++;
});
複製代碼
咱們須要注意處理枚舉的visible
屬性狀況。爲此,咱們須要在 Touch down
事件中添加一些基本條件。
void onTapDown(TapDownDetails tap) {
// User pressed the button. This can be a tap or a hold.
if (scoreOutETA != null) {
scoreOutETA.cancel(); // We do not want the score to vanish!
}
if(_scoreWidgetStatus == ScoreWidgetStatus.BECOMING_INVISIBLE) {
// We tapped down while the widget was flying up. Need to cancel that animation.
scoreOutAnimationController.stop(canceled: true);
_scoreWidgetStatus = ScoreWidgetStatus.VISIBLE;
}
else if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN ) {
_scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;
scoreInAnimationController.forward(from: 0.0);
}
increment(null); // Take care of tap
holdTimer = new Timer.periodic(duration, increment); // Takes care of hold
}
複製代碼
最後,咱們使用Widget
中控制器的值
extraSize = scoreSizeAnimationController.value * 10;
...
height: 50.0 + extraSize,
width: 50.0 + extraSize,
...
複製代碼
完整的代碼,能夠在github(gist.github.com/Kartik1607/… 中找到。咱們同時使用大小和位置動畫。大小動畫須要一些調整,咱們最後會介紹。
在進行火花閃爍動畫
以前,咱們須要對尺寸動畫進行一些調整。目前,該按鈕已增加太多。解決方法很簡單,咱們將額外的乘數從10
更改成一個較小的數字。
如今來看看火花閃爍
動畫,咱們能夠看到到火花
其實就是位置在變化的5
張圖片
我在MS Paint
中製做了一個三角形和一個圓形的圖片,並將其保存爲flutter資源。而後,咱們就能夠將該圖片用做Image asset
。
在實現動畫以前,讓咱們考慮一下定位以及須要完成的一些任務:
一、咱們須要定位5
個圖片,每張圖片以不一樣的角度造成一個完整的圓。
二、咱們須要根據角度旋轉圖片
三、隨着時間增長圓的半徑
四、須要根據角度和半徑找到座標。
簡單的三角函數給了咱們根據角度的正弦和餘弦來得到x
和y
座標的公式。
var sparklesWidget =
new Positioned(child: new Transform.rotate(
angle: currentAngle - pi/2,
child: new Opacity(opacity: sparklesOpacity,
child : new Image.asset("images/sparkles.png", width: 14.0, height: 14.0, ))
),
left:(sparkleRadius*cos(currentAngle)) + 20,
top: (sparkleRadius* sin(currentAngle)) + 20 ,
);
複製代碼
如今,咱們須要建立5
個widget
。每一個widget
具備不一樣的角度。一個簡單的for
循環就ok了。
for(int i = 0;i < 5; ++i) {
var currentAngle = (firstAngle + ((2*pi)/5)*(i));
var sparklesWidget = ...
stackChildren.add(sparklesWidget);
}
複製代碼
將2 * pi
(360度)分紅5
個部分,並相應地建立一個widget
。而後,咱們將widget
添加到stackChildren
數組中。
好了,到這一步,大多數的準備工做都作完了,咱們只須要設置sparkleRadius
的動畫並生成一個新的firstAngle
便可。
sparklesAnimationController = new AnimationController(vsync: this, duration: duration);
sparklesAnimation = new CurvedAnimation(parent: sparklesAnimationController, curve: Curves.easeIn);
sparklesAnimation.addListener((){
setState(() { });
});
void increment(Timer t) {
sparklesAnimationController.forward(from: 0.0);
...
setState(() {
...
_sparklesAngle = random.nextDouble() * (2*pi);
});
Widget getScoreButton() {
...
var firstAngle = _sparklesAngle;
var sparkleRadius = (sparklesAnimationController.value * 50) ;
var sparklesOpacity = (1 - sparklesAnimation.value);
...
}
複製代碼
這就是咱們對flutter中的基本動畫介紹。我將繼續探索flutter,學習建立高級UI。
完整代碼訪問:github.com/Kartik1607/…