1. 基本动画概念和相关类
- Animation: flutter 动画库中的一个核心类,它生成指导动画的值;
- AnimationController: Animation 的管理类。
- CurvedAnimation: 用于定义非线性曲线动画.
- Tween: 补间对象,用于计算动画使用的数据范围之间的插值 (例:Tween 可能会生成从红到蓝之间的色值,或者从0到255) 。
- Listeners 和 StatusListeners: 监听动画状态改变。
Flutter 中的动画系统基于 Animation 对象的,widget 可以在 build 函数中读取 Animation 对象的当前值,并且可以监听动画的状态改变。
1.1. Animation
- Animation 是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常见的Animation 类是 Animation< double >;
- Flutter 中的 Animation 对象是一个在一段时间内依次生成一个区间之间值的类。 Animation 对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。 根据 Animation 对象的控制方式,动画可以反向运行,甚至可以在中间切换方向;
- Animation 还可以生成除 < double > 之外的其他类型值,如:Animation< Color > 或 Animation< Size >;
- Animation 对象有状态。可以通过访问其 value 属性获取动画的当前值。
- Animation 对象本身和UI渲染没有任何关系。
1.2. AnimationController
AnimationController 是 Animation< double > 的子类:
- AnimationController 会生成一系列的值,数字的产生与屏幕刷新有关,因此每秒钟通常会产生60个数字,(默认情况下,AnimationController 在给定的时间段内会线性的生成从0.0到1.0之间的数字);
- 在生成每个数字后,每个 Animation 对象调用添加的 Listener 对象。
代码示例:
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this
);
当创建一个 AnimationController 时,需要传递一个 vsync 必传参数,vsync 对象会绑定动画的定时器到一个可视的 widget,它的作用是避免动画相关UI不在当前屏幕时消耗资源:
- 当 widget 不显示时,动画定时器将会暂停;
- 当 widget 再次显示时,动画定时器重新恢复执行,
- 如果要使用自定义的 State 对象作为 vsync 时,需要包含 TickerProviderStateMixin;
AnimationController 控制动画的方法:
- forward:向前执行动画;
- reverse:反向执行动画;
- stop:停止动画;
1.3. CurvedAnimation
CurvedAnimation 是 Animation< double > 的子类:
- 它可以将 AnimationController 定义为一个非线性曲线动画。
代码示例:
final CurvedAnimation curvedAnimation = CurvedAnimation(
parent: controller,
curve: Curves.linear
);
curve 参数对象的有一些常量Curves(和Color类型有一些Colors是一样的)可以供我们直接使用,例如:linear、easeIn、easeOut、easeInToLinear等等。
1.4. Tween
Tween 继承自 Animatable< T >:
- AnimationController 动画生成的值所在区间默认是是0.0到1.0,如果我们需要不同的范围或不同的数据类型,则可以使用 Tween 来配置动画以生成不同的范围或数据类型的值。
代码示例:
final Tween tweenAnim = Tween(
begin: 50.0,
end: 150.0
).animate(curvedAnimation);
- Tween 是一个无状态 stateless 对象,需要 begin 和 end 值,Tween 的唯一职责就是定义从输入范围到输出范围的映射。
- 要使用 Tween 对象,需要调用 animate() 方法,传入一个 Animation<double> 对象,返回的是 Animation< T > 对象。
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 动画示例-1
主要代码如下:
class LookPage extends StatefulWidget {
@override
_CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _curvedAnimation;
Animation<double> _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(() {});
});
// 4. 监听状态
_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: <Widget>[
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(),这会带来两个问题:
- 执行动画必须包含被部分代码,代码比较冗余;
- 调用 setState() 意味着整个 State 类中的 build 方法就会被重新执行。
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<double> _tweenAnimation;
CJAnimatedWidget(this._tweenAnimation): super(listenable: _tweenAnimation);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
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 子类给我们使用:
- FadeTransition
- ScaleTransition
- RotationTransition
- SizeTransition
- SlideTransition
- RelativePositionedTransition
- DecoratedBoxTransition
- AlignTransition
- DefaultTextStyleTransition
- PositionedTransition
AnimatedWidget 虽然解决了一些问题,但是它也有一些弊端:
- 我们大部分情况都要新建一个类来继承自 AnimatedWidget;
- 如果我们的动画 Widget 有子 Widget,那么意味着它的子 Widget 也会重新 build。
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: <Widget>[
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 动画示例-2
主要代码如下:
class LookPage extends StatefulWidget {
@override
_CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends State<LookPage> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _curvedAnimation;
Animation<double> _glassLocationAnim;
Animation<double> _glassRotationAnim;
Animation<double> _necklaceOpacityAnim;
Animation<double> _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: <Widget>[
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();
}
}
主要思路:
- 给墨镜添加一个位置变化动画和一个旋转动画,给墨镜一个初始位置,开启动画之后,墨镜在旋转的同时移动的目标位置;
- 大金链子则添加一个位置变化动画,开启动画之后,直接移动到目标位置。
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 State<LookPage> with 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 State<LookPage> with SingleTickerProviderStateMixin {
bool _click = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("动画"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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 控件:
- 第一步设置图标;
- 第二步设置 progress,progress 用于图标的动画。
AnimatedIcon 动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override
_CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends State<LookPage> with 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: <Widget>[
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,
),
),
);
}
}
这个不需要思路...
系统动画组件还有很多,但是有些功能是有重叠的,在这里就不一一陈述了。
其他系统动画组件如下:
- AnimatedAlign
- AnimatedDefaultTextStyle
- AnimatedModalBarrier
- AnimatedOpacity
- AnimatedPadding
- AnimatedPhysicalModel
- AnimatedPositioned
- AnimatedPositionedDirectional
- AnimatedSize
4. 转场动画
如果我们要导航到一个新页面,一般会使用 MaterialPageRoute,在页面切换的时候,会有默认的自适应平台的过渡动画,如果想自定义页面的进场和出场动画,那么需要使用 PageRouteBuilder 来创建路由,PageRouteBuilder 主要的部分:
- 一个是“pageBuilder”,用来创建所要跳转到的页面;
- 另一个是“transitionsBuilder”,也就是我们可以自定义的转场效果。
PageRouteBuilder 转场动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override
_CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends State<LookPage> with 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<double>(
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 Animation<double> turns,
this.alignment = Alignment.center,
this.child,
}) : assert(turns != null),
super(key: key, listenable: turns);
Animation<double> get 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,
);
}
}
主要思路:
- 第二个页面出现的时候,围绕 Y 轴旋转90度。
4.1 Hero
Hero 是我们常用的过渡动画,当用户点击一张图片,切换到另一个页面时,这个页面也有此图,那么我们可以使用 Flutter 给我们提供的 Hero 组件来完成这个效果:
- 2个页面都要有 Hero 控件,且保证 tag 参数一致。
Hero 动画示例:
主要代码如下:
class LookPage extends StatefulWidget {
@override
_CJAnimationWidgetState createState() => _CJAnimationWidgetState();
}
class _CJAnimationWidgetState extends State<LookPage> with 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,
),
),
),
),
);
}
}
如有不对的地方,还请指出,感谢!