动画
在flutter中,如果想让某个widget执行动画,需要用一个动画类的widget封装一下,比如一个图片
Center( child: Image.asset(xxxxxx), ), 复制代码
想实现透明度动画,使用FadeTransition,如下
Center( child: FadeTransition( opacity: _speakAnimation, child: Image.asset(R.assetsLiveSpeak), ), ), 复制代码
关键是opacity,是一个Animation,要想实现一个动画,还需要一个Tween和一个AnimationController。
Tween就是Animation。Tween还需要一个AnimationController,他们的创建及定义如下:
class _XXX extends State<XXXX> with SingleTickerProviderStateMixin{ AnimationController _speakController; Animation _speakAnimation; @override void initState() { super.initState(); _speakController = AnimationController( vsync: this, duration: Duration(seconds: 2), ); _speakAnimation = Tween( begin: 1.0, end: 0.0, ).animate(_speakController); _speakAnimation.addStatusListener((status) { //监听动画 if(status == AnimationStatus.completed){ //todo } }); } ... 复制代码
可以看到Tween定义的是动画的起始和结束状态(这里就是透明度的值)。而AnimationController主要是定义时长。
另外Tween可以添加动画监听(addStatusListener),一共有四种状态
enum AnimationStatus { /// The animation is stopped at the beginning. dismissed, /// The animation is running from beginning to end. forward, /// The animation is running backwards, from end to beginning. reverse, /// The animation is stopped at the end. completed, } 复制代码
但是这样还差最后一步,启动动画,因为我们需求是页面一展示即播放,所以在build函数中forward一下即可(当然还可以在其他时机播放),代码如下:
class _XXX extends State<XXXX> with SingleTickerProviderStateMixin{ AnimationController _speakController; Animation _speakAnimation; @override void initState() { super.initState(); _speakController = AnimationController( vsync: this, duration: Duration(seconds: 2), ); _speakAnimation = Tween( begin: 1.0, end: 0.0, ).animate(_speakController); _speakAnimation.addStatusListener((status) { if(status == AnimationStatus.completed){ //todo } }); } ... Widget build(BuildContext context) { _speakController.forward(); //播放动画 return Stack( children: <Widget>[ Center( child: FadeTransition( opacity: _speakAnimation, child: Image.asset(R.assetsLiveSpeak), ), ), ... 复制代码
动画卡顿
从今天的一个需求说起吧,实现一个按钮呼吸效果,很简单,就是使用一个缩放动画即可,如下:
class _xxx extends State<xxx> with SingleTickerProviderStateMixin{ AnimationController _animationController; Animation _animation; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 800), ); _animation = Tween( begin: 0.8, end: 1.0, ).animate(_animationController); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { _animationController.repeat(reverse: true); return Stack( alignment: Alignment.center, children: [ ... Column( children: [ ... ScaleTransition( scale: _animation, child: TextButton( onPressed: widget.onOpen, child: Container( width: 120, height: 60, padding: EdgeInsets.only(bottom: 8), alignment: Alignment.center, decoration: BoxDecoration( image: DecorationImage( image: AssetImage(R.assetsLiveRedPackageBtn) ) ), child: Text( "领取", style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFFB34D35) ), ), ), ), ), ... ], ) ], ); } } 复制代码
动画效果是从0.8倍扩大到1.0,然后reverse,一直repeat即可。
但是运行后发现动画出现了异常,动画扩大到1.0后会快速小幅度缩放一次,然后才会还原到0.8 这明显不是我们想要的动画效果,后来我们尝试了其他动画,都有类似的效果。
但是在其他页面上的动画就不会出问题,所以最后排查发现是该页面的定时器影响到了动画。
这个页面里有一个倒计时,通过Timer来更新其中倒计时的文字,而更新使用了setState进行重绘,这样在动画执行到1秒的时候(扩大到1.0又缩回一点的时候)倒计时更新了,由于是setState,所以动画widget也重绘了(这样又变回1.0大小了),然后才缩回0.8
解决方法就是新增一个StatefulWidget类,将Timer和倒计时相关的组件放到这个类中实现,这样倒计时更新只会刷新这一部分,不会刷新动画组件,如下:
class TimerText extends StatefulWidget{ int time; TimerText(this.time); @override State<StatefulWidget> createState() { return _TimerText(); } } class _TimerText extends State<TimerText>{ Timer timer; @override void initState() { super.initState(); timer = Timer.periodic(Duration(seconds: 1), (timer) { setState(() { if(widget.time <= 0){ timer.cancel(); } else{ widget.time--; } }); }); } @override void dispose() { timer.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Text( "${widget.time}s", style: TextStyle( fontSize: 14, color: Color(0xFFF9D873) ), ); } }