在使用手机的过程中动画效果随处可见,动画设计可以很好地提升用户体验,在 iOS、Android 平台都有各自实现动画效果的方式。在 Flutter 中,动画也是必不可少的内容,并且体验可以达到接近原生的效果。
1 Flutter 动画基本概念
1.1 Animation
Flutter 中的动画基于 Animation 对象,它是一个抽象类,保存了当前动画的值和状态;
Animation 对象在一段时间内依次生成一个区间之间值,比较常见的类型是 Animation<double>,其他的还有 Animation<Color> 或 Animation<Size>;
Animation 对象和 UI 渲染是无关的,UI 对象通过读取 Animation 对象的值和监听状态变化运行 build 函数,然后渲染到屏幕上形成动画效果。
1.2 AnimationController
AnimationController 是 Animation<double> 的子类,具有控制动画的启动、暂停、结束等方法:
AnimationController 需要一个 vsync 必传参数,vsync 对象会绑定动画的定时器到一个可视的 widget,它的作用是避免动画相关 UI 不在当前屏幕时消耗资源:
AnimationController 控制动画的方法:
forward:向前执行动画;
reverse:反向执行动画;
stop:停止动画;
代码示例:
AnimationController controller = AnimationController(
duration: const Duration(seconds: 5),
vsync: this);
复制代码
1.3 CurvedAnimation
CurvedAnimation 是 Animation< double > 的子类,它可以将 AnimationController 定义为一个非线性曲线动画。
curve 参数对象的有一些常量 Curves(和 Color 类型有一些 Colors 是一样的)可以供我们直接使用,例如:linear、easeIn、easeOut、easeInToLinear 等等。
代码示例:
CurvedAnimation curvedAnimation = CurvedAnimation(
parent: controller,
curve: Curves.linear);
复制代码
1.4 Tween
Tween 继承自 Animatable<T>,可以映射不同类型和范围的动画取值。
代码示例:
Tween tweenAnim = Tween(
begin: 50.0,
end: 150.0)
.animate(curvedAnimation);
复制代码
1.5 Listeners 和 StatusListeners
一个 Animation 对象可以拥有 Listeners 和 StatusListeners 监听器,可以用 addListener() 和 addStatusListener() 来添加:
addListener:
只要动画的值发生变化,就会调用所有通过 addListener 添加的监听器;
Listener 最常见的行为是调用 setState() 来触发 UI 重建。
addStatusListener:
当动画的状态发生变化时,例如:开始、结束、向前移动或向后移动,都会通知所有通过 addStatusListener 添加的监听器;
通常情况下,动画会从 dismissed 状态开始,表示它处于变化区间的开始点;
举例来说,从 0.0 到 1.0 的动画在 dismissed 状态时的值应该是 0.0;
动画进行的下一状态可能是 forward(比如从 0.0 到 1.0)或者 reverse(比如从 1.0 到 0.0);
最终,如果动画到达其区间的结束点,则动画会变成 completed 状态。
2 动画应用
2.1 基础动画使用
主要代码如下:
class LookPage extends StatefulWidget {
@override _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends Statewith SingleTickerProviderStateMixin {
AnimationController _controller;
Animation_curvedAnimation;
Animation_tweenAnimation;
@override void initState() {
super.initState();
// 1. 创建 controller
_controller = AnimationController(
duration: Duration(milliseconds: 1000),
vsync: this,
);
// 2. 创建 curvedAnimation
_curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.linear
);
// 3. 创建 tween 配置动画值的范围
_tweenAnimation = Tween(
begin: 1.0,
end: 2.0).animate(_curvedAnimation);
// 4. 添加值监听
_controller.addListener(() {
setState(() {});
});
// 5. 监听状态
_controller.addStatusListener((status) {
print(status);
if (status == AnimationStatus.completed) {
_controller.reverse();
}
else if (status == AnimationStatus.dismissed) {
_controller.forward();
}
});
}
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("动画"),
),
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children:[
SizedBox(height: 200,),
// 长度变化
Container(
color: Colors.blueAccent,
width: 100 * _tweenAnimation.value,
height: 60,
),
SizedBox(height: 20,),
// 透明度变化
Opacity(
opacity: 2.0 - _tweenAnimation.value,
child: Container(
color: Colors.blueAccent,
width: 100,
height: 100,
),
),
SizedBox(height: 20,),
// 字体大小变化
Text("窗外风好大",
style: TextStyle(
fontSize: 20 * _tweenAnimation.value,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.done_outline),
onPressed: (){
_controller.forward();
},
),
);
}
@override void dispose() { _controller.dispose(); super.dispose(); }
}
复制代码
根据动画值的变化修改 widget 宽度、透明度和字体大小,虽然动画效果做到了,但是我们必须监听动画值的改变,并且改变后需要调用 setState(),这会带来两个问题:
2.1.1 AnimatedWidget
为了解决上面的问题,我们可以使用 AnimatedWidget (而不是 addListener() 和 setState() )来给 widget 添加动画:
AnimatedWidget 从 setState() 调用中的动画代码中分离出 widget 代码,创建一个可重用动画的 widget;
AnimatedWidget 不需要维护一个 State 对象来保存动画;
AnimatedWidget 中会自动调用 addListener() 和 setState()。
所以上面的代码可以优化成下面这样:
// 使用处
...
body:CJAnimatedWidget(_tweenAnimation),
...
class CJAnimatedWidget extends AnimatedWidget {
final Animation_tweenAnimation;
CJAnimatedWidget(this._tweenAnimation):super(listenable:_tweenAnimation);
@override
Widget build(BuildContext context) {
return Container(
child:Column(
crossAxisAlignment:CrossAxisAlignment.start,
mainAxisAlignment:MainAxisAlignment.start,
children:[
SizedBox(height:200,),
// 长度变化
Container(
color:Colors.blueAccent,
width:100 * _tweenAnimation.value,
height:60,
),
SizedBox(height:20,),
// 透明度变化
Opacity(
opacity:2.0 - _tweenAnimation.value,
child:Container(
color:Colors.blueAccent,
width:100,
height:100,
),
),
SizedBox(height:20,),
// 字体大小变化
Text("窗外风好大",
style:TextStyle(
fontSize:20 * _tweenAnimation.value,
),
),
],
),
);
}
}
复制代码
Flutter 提供了很多封装完成的 AnimatedWidget 子类给我们使用:
AnimatedWidget 虽然解决了一些问题,但是它也有一些弊端:
2.1.2 AnimatedBuilder
为了优化上述问题,我们可以使用 AnimatedBuilder,它可以从 widget 中分离出动画过渡:
AnimatedBuilder 是渲染树中的一个独立的类。与 AnimatedWidget 类似, AnimatedBuilder 自动监听来自 Animation 对象的通知,并根据需要将该控件树标记为脏(dirty),因此不需要手动调用 addListener()。
优化后的代码如下:
...
body: Container(
child: AnimatedBuilder(
animation: _tweenAnimation,
builder: (ctx, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(height: 200,),
// 长度变化
Container(
color: Colors.blueAccent,
width: 100 * _tweenAnimation.value,
height: 60,
),
SizedBox(height: 20,),
// 透明度变化
Opacity(
opacity: 2.0 - _tweenAnimation.value,
child: Container(
color: Colors.blueAccent,
width: 100,
height: 100,
),
),
SizedBox(height: 20,),
// 字体大小变化
Text("窗外风好大",
style: TextStyle(
fontSize: 20 * _tweenAnimation.value,
),
),
],
);
},
),
),
...
复制代码
2.2 动画组合使用
主要代码如下:
class LookPage extends StatefulWidget {
@override _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends Statewith SingleTickerProviderStateMixin {
AnimationController _controller;
Animation_curvedAnimation;
Animation_glassLocationAnim;
Animation_glassRotationAnim;
Animation_necklaceOpacityAnim;
Animation_necklaceLocationAnim;
@override void initState() {
super.initState();
// 1. 创建 controller
_controller = AnimationController(
duration: Duration(milliseconds: 2000),
vsync: this,
);
// 2. 创建 curvedAnimation
_curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.linear
);
// 3. 创建 tween 配置动画值的范围
_glassLocationAnim = Tween(
begin: 0.0,
end: 252.0
).animate(_curvedAnimation);
_glassRotationAnim = Tween(
begin: 0.0,
end: 2.1*pi ).animate(_curvedAnimation);
_necklaceLocationAnim = Tween(
begin: 500.0,
end: 370.0 ).animate(_curvedAnimation);
_necklaceOpacityAnim = Tween(
begin: 0.0,
end: 1.0 ).animate(_curvedAnimation);
}
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("动画"),
),
body: AnimatedBuilder(
animation: _controller,
builder: (ctx, child) {
return Stack(
overflow: Overflow.clip,
children:[
Positioned(
left: 0,
top: 200,
width: 414,
child: Image.asset(
"assets/images/dog.jpg",
width: 414,
)
),
Positioned(
left: _glassLocationAnim.value - 130,
top: 207,
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationZ(_glassRotationAnim.value),
child: Image.asset(
"assets/images/glasses.png",
width: 130,
height: 130,
),
)
),
Positioned(
left: 130,
top: _necklaceLocationAnim.value,
child: Opacity(
opacity: 1 * _necklaceOpacityAnim.value,
child: Image.asset(
"assets/images/necklace.png",
width: 160,
height: 110,
),
)
),
],
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.done_outline),
onPressed: (){
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else if (_controller.status == AnimationStatus.dismissed) {
_controller.forward();
}
},
),
);
} @override void dispose() {
_controller.dispose();
super.dispose();
}}
复制代码
主要思路:
创建 Tween 配置动画取值范围,如墨镜的移动位置、旋转角度,金链子的移动位置、透明度;
墨镜添加“位置变化和旋转”动画,给墨镜一个初始位置,开启动画之后,墨镜在围绕 Z 轴旋转的同时移动到目标位置;
大金链子则添加“位置变化和透明度”动画,开启动画之后,改变透明度的同时移动到目标位置。
3 系统动画组件
3.1 AnimatedContainer
我们可以理解 AnimatedContainer 是带动画功能的 Container:
AnimatedContainer 只需要提供动画开始值和结束值,它就会动起来并不需要我们主动调用 setState() 方法;
动画不仅可以作用在宽高上,还可以作用在颜色、边界、边界圆角半径、背景图片、形状等;
AnimatedContainer 有 2 个必须的参数,一个时长 duration,即动画执行的时长,另一个是动画曲线 curve,默认是线性,系统为我们提供了很多动画曲线(加速、减速等),例:
curve: Curves.bounceIn
AnimatedContainer 动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends Statewith SingleTickerProviderStateMixin {
bool _click = false;
@override Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
setState(() {
_click = !_click;
});
},
child: AnimatedContainer(
height: _click ? 200 : 100,
width: _click ? 200 : 100,
duration: Duration(milliseconds: 2000),
curve: Curves.easeInOutCirc,
transform: Matrix4.rotationX(_click ? pi : 0),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/images/girl.jpg"),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.all(Radius.circular(
_click ? 200 : 100,
))
),
onEnd: () {
setState(() {
_click = !_click;
});
},
),
),
);
}}
复制代码
主要思路 :根据点击的状态变化,修改图片的尺寸以及图片围绕 X 轴旋转的角度。
3.2 AnimatedCrossFade
AnimatedCrossFade 组件让 2 个组件在切换时出现交叉渐入的效果,因此 AnimatedCrossFade 需要设置 2 个子控件、动画时间和显示第几个子控件。
AnimatedCrossFade 动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends Statewith SingleTickerProviderStateMixin {
bool _click = false;
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("动画"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children:[
AnimatedContainer(
duration: Duration(seconds: 2),
width: 200,
height: _click ? 200 : 100,
decoration: BoxDecoration(
color: _click ? Colors.blueAccent : Colors.green,
borderRadius: BorderRadius.all(
Radius.circular(_click ? 0 : 50,)
)
),
),
SizedBox(height: 20,),
AnimatedCrossFade(
duration: Duration(seconds: 2),
crossFadeState: _click ? CrossFadeState.showSecond : CrossFadeState.showFirst,
firstChild: Container(
height: 100,
width: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: Colors.green
),
child: Text('1',
style: TextStyle(
color: Colors.white,
fontSize: 40
),
),
),
secondChild: Container(
height: 200,
width: 200,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.blueAccent,
),
child: Text('2',
style: TextStyle(
color: Colors.white,
fontSize: 40
),
),
),
),
SizedBox(height: 20,),
AnimatedContainer(
duration: Duration(seconds: 2),
width: 200,
height: _click ? 200 : 100,
decoration: BoxDecoration(
color: _click ? Colors.blueAccent : Colors.green,
borderRadius: BorderRadius.all(
Radius.circular(_click ? 0 : 50,)
)
),
),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.done_outline),
onPressed: () {
setState(() {
_click = !_click;
});
},
)
);
}}
复制代码
主要思路: 根据点击的状态变化,改变 AnimatedCrossFade 显示的子控件,同时让上下的两个 AnimatedContainer 变化形状。
3.3 AnimatedIcon
Flutter 还提供了很多动画图标,想要使用这些动画图标需要使用 AnimatedIcon 控件:
AnimatedIcon 动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends Statewith SingleTickerProviderStateMixin {
AnimationController _controller;
@override void initState() {
super.initState();
// 1. 创建 controller
_controller = AnimationController(
duration: Duration(milliseconds: 2000),
vsync: this,
);
// 2. 监听状态
_controller.addStatusListener((status) {
print(status);
});
}
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("动画"),
),
body: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
children:[
createAnimatedIcon(AnimatedIcons.add_event),
createAnimatedIcon(AnimatedIcons.arrow_menu),
createAnimatedIcon(AnimatedIcons.close_menu),
createAnimatedIcon(AnimatedIcons.ellipsis_search),
createAnimatedIcon(AnimatedIcons.event_add),
createAnimatedIcon(AnimatedIcons.home_menu),
createAnimatedIcon(AnimatedIcons.list_view),
createAnimatedIcon(AnimatedIcons.menu_arrow),
createAnimatedIcon(AnimatedIcons.menu_close),
createAnimatedIcon(AnimatedIcons.menu_home),
createAnimatedIcon(AnimatedIcons.pause_play),
createAnimatedIcon(AnimatedIcons.play_pause),
createAnimatedIcon(AnimatedIcons.search_ellipsis),
createAnimatedIcon(AnimatedIcons.view_list),
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.done_outline),
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else if (_controller.status == AnimationStatus.dismissed) {
_controller.forward();
}
},
)
);
}
Widget createAnimatedIcon (AnimatedIconData animIconData) {
return Container(
width: 138,
height: 138,
child: Center(
child: AnimatedIcon(
icon: animIconData,
progress: _controller,
),
),
);
}}
复制代码
系统动画组件还有很多,但是有些功能是有重叠的,在这里就不一一陈述了。
其他系统动画组件如下:
4 转场动画
4.1 PageRouteBuilder
如果我们要导航到一个新页面,一般会使用 MaterialPageRoute,在页面切换的时候,会有默认的自适应平台的过渡动画,如果想自定义页面的进场和出场动画,那么需要使用 PageRouteBuilder 来创建路由,PageRouteBuilder 主要的部分:
PageRouteBuilder 转场动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override _CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends Statewith SingleTickerProviderStateMixin {
String _imageURL = "assets/images/cj3.png";
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第一页"),
backgroundColor: Color.fromARGB(255, 24,45, 105),
),
body: Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return CJNextPage("assets/images/cj2.png");
},
transitionsBuilder: (context, animation, secondaryAnimation, child){
return CJRotationTransition(
turns: Tween(
begin: 1.0,
end: 0.0
).animate(animation),
child: child,
);
}
)
);
},
child: Image.asset(_imageURL, height: 896, fit: BoxFit.fitHeight,)
),
),
);
}}
class CJNextPage extends StatelessWidget {
final String imageURL;
CJNextPage(this.imageURL);
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
backgroundColor: Colors.lightBlueAccent,
),
backgroundColor: Colors.white,
body: Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Image.asset(imageURL, height: 896, fit: BoxFit.fitHeight),
),
),
);
}}
class CJRotationTransition extends AnimatedWidget {
const CJRotationTransition({
Key key,
@required Animationturns,
this.alignment = Alignment.center,
this.child,
}) : assert(turns != null),
super(key: key, listenable: turns);
Animationget turns => listenable;
final Alignment alignment;
final Widget child;
@override Widget build(BuildContext context) {
final double turnsValue = turns.value;
final Matrix4 transform = Matrix4.rotationY(turnsValue * pi/2.0);
return Transform(
transform: transform,
alignment: alignment,
child: child,
);
}}
复制代码
主要思路: 自定义 CJRotationTransition 类,使第二个页面出现的时候,围绕 Y 轴旋转 90 度。
4.2 Hero
Hero 是我们常用的过渡动画,当用户点击一张图片,切换到另一个页面时,这个页面也有此图,那么我们可以使用 Flutter 给我们提供的 Hero 组件来完成这个效果:
2 个页面都要有 Hero 控件,且保证 tag 参数一致。
Hero 动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override _CJAnimationWidgetState createState() => _CJAnimationWidgetState();}
class _CJAnimationWidgetState extends Statewith SingleTickerProviderStateMixin {
String _imageURL = "assets/images/shoes.JPG";
@override Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第一页"),
backgroundColor: Color.fromARGB(255, 24, 45, 105),
),
body: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
children: List.generate(20, (index) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return CJHeroPage(_imageURL, "$_imageURL-$index");
},
transitionsBuilder: (context, animation, secondaryAnimation,
child) {
return FadeTransition(
opacity: animation,
child: child,
);
}
)
);
},
child: Hero(
tag: "$_imageURL-$index",
child: Image.asset(_imageURL, width: 125, height: 125,),
)
);
}),
),
);
}}
class CJHeroPage extends StatelessWidget {
final String imageURL;
final String heroTag;
CJHeroPage(this.imageURL, this.heroTag);
@override Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Center(
child: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Hero(
tag: heroTag,
child: Image.asset(imageURL, width: double.infinity, fit: BoxFit.cover, ),
),
),
),
);
}}
复制代码
5 总结
本文主要介绍了 Flutter 动画的基本概念和应用。制作动画并不难,但是想要制作一个完整的动画效果就需要耐心的去调整每个细节。加油,奥利给!
本文转载自公众号贝壳产品技术(ID:beikeTC)。
原文链接:
如何玩转Flutter动画
评论