在使用手机的过程中动画效果随处可见,动画设计可以很好地提升用户体验,在 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动画
评论