前言:
本文將自定義一個FlutterWidget的動畫元件,Flutter有顫動的意思
在此之前會講一下AnimatedWidget與AnimatedBuilder是什麼,如何使用
所以本文是一篇挺重要的文章,不僅是內容,還有思想和靈魂。
今天也悟到了一段話分享給大家:
當你遇到一群共事之人,開始難免會覺得某某人高冷而帥氣,某某人美麗而大方,某某人能力超級強
作為普通人的你也許很想和他們結交但又很難進入他們的世界,於是你在角落靜靜凝望,細心觀察
隨著時間的流逝,也許偶爾的交談,你會發現他們並非看上去的那麼難以接近,於是開始和他們交流
隨著關係的加深,也許某個傍晚,你們會走在回去的路上,訴說著人生,從此漸漸無話不說。
然後會發現,這世間的隔閡也許只是自己為自己施加的屏障,這個屏障會為你抵禦傷害,
但它同時也可能讓你失去一個對的人,一個未來的止步於陌生的知己。
學習亦如此,一個框架就是那個高冷而帥氣公子,一個類就是那個美麗而大方姑娘,結合上面再看看。
有時候錯過了,也就錯過了,你不可能認識所有的人,但你可以用真誠選擇一位知己。
認識的人當然越多越好,但知己,寧缺毋濫。 ----XXX,你現在還好嗎?
(張風捷特烈 2019.7.19 字)
複製程式碼
首先,留圖鎮樓
1.AnimatedWidget與AnimatedBuilder
1.1:前情回顧
現在回到昨天的最後一個元件,這樣寫不夠優雅,什麼東西都在一塊
Flutter中提供了AnimatedWidget類可以讓動畫的元件更加簡潔
class FlutterText extends StatefulWidget {
var str;
var style;
FlutterText(this.str, this.style);
_FlutterTextState createState() => _FlutterTextState();
}
class _FlutterTextState extends State<FlutterText>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = TweenSequence<double>([//使用TweenSequence進行多組補間動畫
TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
]).animate(controller)
..addListener(() {
setState(() {});
})
..addStatusListener((s) {
if (s == AnimationStatus.completed) {
setState(() {});
}
});
controller.forward();
}
Widget build(BuildContext context) {
var result = Transform(
transform: Matrix4.rotationZ(animation.value * pi / 180),
alignment: Alignment.center,
child: Text(
widget.str,
style: widget.style,
),
);
return result;
}
dispose() {
controller.dispose();
super.dispose();
}
}
複製程式碼
2.使用AnimatedWidget抽離元件
AnimatedWidget也不是什麼神奇的東西,它的優勢在於:
將元件的建立邏輯單獨封裝在一個類中,而且不用再呼叫setState方法
,也能自動更新資訊
class FlutterText extends StatefulWidget {
var str;
var style;
FlutterText(this.str, this.style);
_FlutterTextState createState() => _FlutterTextState();
}
class _FlutterTextState extends State<FlutterText>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = TweenSequence<double>([//使用TweenSequence進行多組補間動畫
TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
]).animate(controller);
controller.forward();
}
Widget build(BuildContext context) {
return AnimateWidget(animation: animation);
}
dispose() {
controller.dispose();
super.dispose();
}
}
class AnimateWidget extends AnimatedWidget{
AnimateWidget({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
var result = Transform(
transform: Matrix4.rotationZ(animation.value * pi / 180),
alignment: Alignment.center,
child: Text(
"捷",
style: TextStyle(fontSize: 50),
),
);
return result;
}
}
複製程式碼
可以看出程式碼明確了很多,AnimateWidget專門負責Widget的構建
FlutterText只注重Animation構成,分工明確,易於複用、維護和擴充
3.使用AnimatedBuilder抽離動畫
AnimatedWidget不挺好的嗎,又來一個AnimatedBuilder什麼鬼
AnimateWidget負責元件的抽離,可以看出元件中雜糅了動畫邏輯
而AnimatedBuilder恰好相反,它不在意元件是什麼,只是將動畫抽離達到複用簡單
這樣針對不同的元件,都可以產生同樣的動畫效果,比如傳入一個Image
class FlutterText extends StatefulWidget {
final Widget child;
FlutterText({this.child});
_FlutterTextState createState() => _FlutterTextState();
}
class _FlutterTextState extends State<FlutterText>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = TweenSequence<double>([ //使用TweenSequence進行多組補間動畫
TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
]).animate(controller);
controller.forward();
}
Widget build(BuildContext context) {
return FlutterAnim(animation: animation,child: widget.child,);
}
dispose() {
controller.dispose();
super.dispose();
}
}
class FlutterAnim extends StatelessWidget {
FlutterAnim({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) {
var result = AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Transform(
transform: Matrix4.rotationZ(animation.value * pi / 180),
alignment: Alignment.center,
child: this.child);
},
);
return Center(child: result,);
}
}
---->[使用]----
var child = Image(
image: AssetImage("images/icon_head.png"),
);
var scaffold = Scaffold(
body: Center(child: FlutterText(child: child),),
);
var app = MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home:scaffold,
);
void main() => runApp(app);
複製程式碼
可以看到,現在不止針對於文字,對於所有的Widget都有效,實現了功能的更高層抽象。
2.元件之所為元件
2.1:元件是什麼
模組化的思想大家應該都聽過,為了讓已有程式碼更好複用,將專案拆成不同模組
元件也是這樣,對於一個頁面,便是元件的組合,可以拆裝,拼湊和批量生成
在程式碼中我們可以很輕易的將多個控制元件批量動效。比如一段話的每個字都有效果:
_formChild(String str) {
var li = <Widget>[];
for (var i = 0; i < str.length; i++) {
li.add(FlutterText(child: Text(str[i],style: TextStyle(fontSize: 30),),
));
}
return li;
}
var textZone=Row(children:_formChild("程式碼,改變生活"),mainAxisSize: MainAxisSize.min,);
複製程式碼
使用_formChild批量生成單個文字,每個文字都加有抖動的光環,所以呈現每個字都抖動的效果
2.2:FlutterText的修改與封裝
現在類名叫FlutterText有點不妥了,它包含一個孩子,可以讓其中的孩子抖動,改名:
FlutterLayout
那現在想讓每個文字都抖一下,每次都寫這麼多也不爽,所以可以單獨封裝一下
這裡FlutterText繼承自Text,並定義所有屬性。在build方法裡生成剛才的帶有顫動效果的元件
class FlutterText extends Text {
const FlutterText(
this.data, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
}) : super(data);
final String data;
final TextStyle style;
final StrutStyle strutStyle;
final TextAlign textAlign;
final TextDirection textDirection;
final Locale locale;
final bool softWrap;
final TextOverflow overflow;
final double textScaleFactor;
final int maxLines;
final String semanticsLabel;
final TextWidthBasis textWidthBasis;
@override
Widget build(BuildContext context) {
var textZone = Row(
children: _formChild(data),
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
);
return textZone;
}
_formChild(String str) {
var li = <Widget>[];
for (var i = 0; i < str.length; i++) {
li.add(FlutterLayout(
child: Text(
str[i],
style: style,
strutStyle: strutStyle,
textAlign: textAlign,
textDirection: textDirection,
locale: locale,
softWrap: softWrap,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
semanticsLabel: semanticsLabel,
textWidthBasis: textWidthBasis,
),
));
}
return li;
}
}
複製程式碼
2.3:FlutterText的使用
你可以完全當它是一個Text來用,只不過有個抖動的效果
var child = Image(
image: AssetImage("images/icon_head.png"),
);
var text = FlutterText("程式碼,改變生活", style: TextStyle(
color: Colors.blue,
fontSize: 30,
letterSpacing: 3
),);
var scaffold = Scaffold(
body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[child, text],
),),
);
var app = MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: scaffold,
);
void main() => runApp(app);
複製程式碼
這樣一個抖動的Text就完成了,本文結束了嗎?不,才剛剛開始。
2.升級FlutterLayout的功能
2.1.抖動樣式:RockMode
分上下抖動,左右抖動,搖擺抖動,隨機抖動
enum RockMode {
random, //隨機
up_down, //上下
left_right, //左右
lean //傾斜
}
複製程式碼
2.2.定義配置引數:AnimConfig
class AnimConfig {//動畫配置
int duration;//時長
double offset;//偏移大小
RockMode mode;//搖晃模式
AnimConfig({this.duration, this.offset, this.mode});
}
複製程式碼
2.3.FlutterLayout具體實現
這裡只是把常量配置引數化,在生成_formTransform的時候根據模式來生成
class FlutterLayout extends StatefulWidget {
final Widget child;
final AnimConfig config;
FlutterLayout({this.child, this.config});
_FlutterLayoutState createState() => _FlutterLayoutState();
}
class _FlutterLayoutState extends State<FlutterLayout>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: Duration(milliseconds: widget.config.duration), vsync: this);
var dx = widget.config.offset;
var sequence = TweenSequence<double>([
//使用TweenSequence進行多組補間動畫
TweenSequenceItem<double>(tween: Tween(begin: 0, end: dx), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: dx, end: -dx), weight: 2),
TweenSequenceItem<double>(tween: Tween(begin: -dx, end: dx), weight: 3),
TweenSequenceItem<double>(tween: Tween(begin: dx, end: 0), weight: 4),
]);
animation = sequence.animate(controller)
..addStatusListener((s) {
if (s == AnimationStatus.completed) {}
});
controller.forward();
}
Widget build(BuildContext context) {
return FlutterAnim(
animation: animation, child: widget.child, config: widget.config);
}
dispose() {
controller.dispose();
super.dispose();
}
}
class FlutterAnim extends StatelessWidget {
FlutterAnim({this.child, this.animation, this.config});
Random random = Random();
final Widget child;
final Animation<double> animation;
final AnimConfig config;
Widget build(BuildContext context) {
var result = AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Transform(
transform: _formTransform(config),
alignment: Alignment.center,
child: this.child);
},
);
return Center(
child: result,
);
}
_formTransform(AnimConfig config) {//分類獲取
var result;
switch (config.mode) {
case RockMode.random:
result = Matrix4.rotationZ(animation.value * pi / 180);
break;
case RockMode.up_down:
result = Matrix4.translationValues(0, animation.value*pow(-1, random.nextInt(20)), 0);
break;
case RockMode.left_right:
result = Matrix4.translationValues(animation.value*pow(-1, random.nextInt(20)), 0, 0);
break;
case RockMode.lean:
result = Matrix4.rotationZ(animation.value * pi / 180);
break;
}
return result;
}
}
複製程式碼
2.4.FlutterText的修改
class FlutterText extends Text {
FlutterText(this.data, {
//略同...
this.config,
}) : super(data);
final AnimConfig config;
Random random = Random();
_formChild(String str) {
var li = <Widget>[];
for (var i = 0; i < str.length; i++) {
li.add(FlutterLayout(
config: AnimConfig(duration: config.duration,offset: config.offset,mode: _dealRandom()),
child: Text(
//略同...
),
));
}
return li;
}
RockMode _dealRandom() {
var modes = [RockMode.lean, RockMode.up_down, RockMode.left_right];
return modes[random.nextInt(3)];
}
}
複製程式碼
2.5:使用MultiShower測試一下
關於MultiShower,可以看一下Flutter自定義元件-MultiShower,主要用於批量產生不同配置的同類元件
var configs=<AnimConfig>[
AnimConfig(duration: 1000,offset: 4,mode: RockMode.random),
AnimConfig(duration: 1000,offset: 4,mode: RockMode.up_down),
AnimConfig(duration: 1000,offset: 4,mode: RockMode.left_right),
AnimConfig(duration: 1000,offset: 5,mode: RockMode.lean),
];
var configsInfo=["random","up_down","left_right","lean"];
var show = MultiShower(configs,(config) =>FlutterText("程式碼,改變生活",
config:config,
style: TextStyle(
color: Colors.blue,
fontSize: 30,
letterSpacing: 3
),),infos: configsInfo,width: 250,color: Colors.transparent,);
var scaffold = Scaffold(
body: Center(child: show,)
);
複製程式碼
另外還有我們的FlutterLayout,可以包含任意元件,那Image來測試
var child = Image(
image: AssetImage("images/icon_head.png"),
);
var configs=<AnimConfig>[
AnimConfig(duration: 1000,offset: 4,mode: RockMode.up_down),
AnimConfig(duration: 1000,offset: 4,mode: RockMode.left_right),
AnimConfig(duration: 1000,offset: 5,mode: RockMode.lean),
];
var configsInfo=["up_down","left_right","lean"];
var show = MultiShower(configs,(config) =>FlutterLayout(child: child,
config:config,
),infos: configsInfo,width: 200,color: Colors.transparent,);
var scaffold = Scaffold(
body: Center(child: show,)
);
複製程式碼
好了,到這也差不多了,你以為結束了,稍安勿躁,還有一點
3.增加運動曲線
可以用曲線補間來讓動畫的執行不那麼古板
3.1:程式碼修改
class AnimConfig {//動畫配置
int duration;//時長
double offset;//偏移大小
RockMode mode;//搖晃模式
CurveTween curveTween;//運動曲線
AnimConfig({this.duration, this.offset, this.mode,this.curveTween});
}
class _FlutterLayoutState extends State<FlutterLayout>
with SingleTickerProviderStateMixin {
var curveTween = widget.config.curveTween;
animation = sequence.animate(curveTween==null?controller:curveTween.animate(controller))
..addStatusListener((s) {
if (s == AnimationStatus.completed) {}
});
複製程式碼
3.2:MultiShower測試
Curves內建四十幾種曲線,這裡就隨便挑一些,你也可以用MultiShower自己玩一玩
var child = Image(
image: AssetImage("images/icon_head.png"),
);
var configs = <CurveTween>[
CurveTween(curve: Curves.bounceIn),
CurveTween(curve: Curves.bounceInOut),
CurveTween(curve: Curves.bounceOut),
CurveTween(curve: Curves.decelerate),
CurveTween(curve: Curves.ease),
CurveTween(curve: Curves.easeIn),
CurveTween(curve: Curves.easeInBack),
CurveTween(curve: Curves.easeInCirc),
CurveTween(curve: Curves.easeInCubic),
CurveTween(curve: Curves.easeInExpo),
CurveTween(curve: Curves.easeInOut),
CurveTween(curve: Curves.easeInOutBack),
CurveTween(curve: Curves.easeOut),
CurveTween(curve: Curves.easeOutBack),
CurveTween(curve: Curves.linear),
CurveTween(curve: Curves.linearToEaseOut),
];
var configsInfo = <String>[
"bounceIn","bounceInOut","bounceOut","decelerate",
"ease","easeIn","easeInBack","easeInCirc","easeInCubic",
"easeInExpo","easeInOut","easeInOutBack",
"easeOut","easeOutBack",linear","linearToEaseOut",
];
var show = MultiShower(configs, (config) =>
FlutterLayout(child: child,
config: AnimConfig(
duration: 2000, offset: 45, mode: RockMode.lean, curveTween: config),
), width: 60, color: Colors.transparent,infos: configsInfo,);
複製程式碼
3.3:動畫完成的監聽
定義一個FinishCallback回撥作為配置引數,在animation.addStatusListener裡回撥
class AnimConfig {//動畫配置
int duration;//時長
double offset;//偏移大小
RockMode mode;//搖晃模式
CurveTween curveTween;//運動曲線
FinishCallback onFinish;
AnimConfig({this.duration, this.offset, this.mode,this.curveTween,this.onFinish});
}
typedef FinishCallback = void Function();
---->[_FlutterLayoutState]----
animation = sequence.animate(curveTween==null?controller:curveTween.animate(controller))
..addStatusListener((s) {
if (s == AnimationStatus.completed) {
if(widget.config.onFinish!=null)
widget.config.onFinish();
}
});
複製程式碼
好了,到這裡,本文完結散花。看到這的,贊點起來。
結語
本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328
,期待與你的交流與切磋。
本文所有原始碼見
github/flutter_journey