前言:
本文将自定义一个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
with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = TweenSequence([//使用TweenSequence进行多组补间动画
TweenSequenceItem(tween: Tween(begin: 0, end: 15), weight: 1),
TweenSequenceItem(tween: Tween(begin: 15, end: 0), weight: 2),
TweenSequenceItem(tween: Tween(begin: 0, end: -15), weight: 3),
TweenSequenceItem(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
with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = TweenSequence([//使用TweenSequence进行多组补间动画
TweenSequenceItem(tween: Tween(begin: 0, end: 15), weight: 1),
TweenSequenceItem(tween: Tween(begin: 15, end: 0), weight: 2),
TweenSequenceItem(tween: Tween(begin: 0, end: -15), weight: 3),
TweenSequenceItem(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 animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation 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
with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = TweenSequence([ //使用TweenSequence进行多组补间动画
TweenSequenceItem(tween: Tween(begin: 0, end: 15), weight: 1),
TweenSequenceItem(tween: Tween(begin: 15, end: 0), weight: 2),
TweenSequenceItem(tween: Tween(begin: 0, end: -15), weight: 3),
TweenSequenceItem(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 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 = [];
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 = [];
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: [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
with SingleTickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: Duration(milliseconds: widget.config.duration), vsync: this);
var dx = widget.config.offset;
var sequence = TweenSequence([
//使用TweenSequence进行多组补间动画
TweenSequenceItem(tween: Tween(begin: 0, end: dx), weight: 1),
TweenSequenceItem(tween: Tween(begin: dx, end: -dx), weight: 2),
TweenSequenceItem(tween: Tween(begin: -dx, end: dx), weight: 3),
TweenSequenceItem(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 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 = [];
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(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(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
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(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 = [
"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