Flutter 动画:用 Flutter 来实现一个拍手动画
]),child: new ImageIcon(new AssetImage("images/clap.png"), color: Colors.pink,size: 40.0),));}
@overrideWidget build(BuildContext context) {return new Scaffold(appBar: new AppBar(title: new Text(widget.title),),body: new Center(child: new Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[new Text('You have pushed the button this many times:',),new Text('$_counter',style: Theme.of(context).textTheme.display1,),],),),floatingActionButton: new Padding(padding: new EdgeInsets.only(right: 20.0),child: new Stack(alignment: FractionalOffset.center,overflow: Overflow.visible,children: <Widget>[getScoreButton(),getClapButton(),],)),);}}
看了上面最终的效果图,我们需要做 2 件事:
更改
widgets
的大小。按下按钮时显示分数
widget
,释放按钮时将其隐藏。添加这些小巧的
widget
并为其设置动画。
让我们一个接一个地慢慢增加学习曲线。首先,我们需要了解有关 Flutter 动画的一些基本知识。
了解 Flutter 中基本动画的组件
动画不过是随着时间变化的一些值,例如,当我们点击按钮时,我们希望用动画来让显示分数widget
从底部升起,而当手指离开按钮时,继续上升然后隐藏。
如果仅看分数Widget
,我们需要在一段时间内更改Widget
的位置和不透明度值。
new Positioned(child: new Opacity(opacity: 1.0,child: new Container(...)),bottom: 100.0);
假设我们希望分数 widget 需要 150 毫秒才能从底部显示出来。在以下时间轴上考虑一下:
这是一个简单的 2D 图形。 position
将随着时间而改变。 请注意,对角线是直线。如果你喜欢,它也可以是曲线。
你可以使position
随时间缓慢增加,然后变得越来越快。或者,你也可以让它以超高速进入,然后在最后放慢速度。
下面是我们介绍的第一个组件:Animation Controller
。
scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);
在这里,我们为动画创建了一个简单的控制器(Controller
)。我们已经指定希望动画运行150ms
。但是,vsync
是什么东西?
移动设备每隔几毫秒刷新一次屏幕。这就是我们将一组图像视为连续流或电影的方式。
屏幕刷新的速率因设备而异。 假设移动设备每秒刷新屏幕 60 次(每秒 60 帧)。 那就是每 16.67 毫秒之后,我们就会向大脑提供新的图像。 有时,图像就会错位(在屏幕刷新时发出不同的图像),并且看到屏幕撕裂。 VSync
就是解决这个问题的。
我们给控制器
设置一个监听器,然后开始动画:
scoreInAnimationController.addListener(() {print(scoreInAnimationController.value);});scoreInAnimationController.forward(from: 0.0);/* OUTPUTI/flutter ( 1913): 0.0I/flutter ( 1913): 0.0I/flutter ( 1913): 0.22297333333333333I/flutter ( 1913): 0.3344533333333333I/flutter ( 1913): 0.4459333333333334I/flutter ( 1913): 0.5574133333333334I/flutter ( 1913): 0.6688933333333335I/flutter ( 1913): 0.7803666666666668I/flutter ( 1913): 0.8918466666666668I/flutter ( 1913): 1.0*/
控制器在150ms
内生成了0.0
到1.0
的数字。请注意,生成的值几乎是线性的。 0.2
、0.3
、0.4
…我们如何改变这种行为?这将在第二部分完成:曲线动画
。
曲线动画
bounceInAnimation = new CurvedAnimation(parent: scoreInAnimationController, curve: Curves.bounceIn);bounceInAnimation.addListener(() {print(bounceInAnimation.value);});
/*OUTPUTI/flutter ( 5221): 0.0I/flutter ( 5221): 0.0I/flutter ( 5221): 0.24945376519722218I/flutter ( 5221): 0.16975716286388898I/flutter ( 5221): 0.17177866222222238I/flutter ( 5221): 0.6359024059750003I/flutter ( 5221): 0.9119433941222221I/flutter ( 5221): 1.0*/
通过将parent
属性设置为我们的控制器,并提供动画遵循曲线,就可以创建一个CurvedAnimation
,Flutter 曲线文档页面上提供了多种曲线供我们选择:api.flutter.dev/flutter/ani…
控制器在150ms
的时间内为曲线动画Widget
提供从0.0
到1.0
的值。曲线动画Widget
根据我们设置的曲线对这些值进行插值。
尽管我们得到了0.0
到1.0
之间的一系列值,但是我们希望显示分数的Widget
显示的值为0-100
,我们可以简单地乘以 100 来得到结果,或者我们可以使用第三个组件:Tween
类。
tweenAnimation = new Tween(begin: 0.0, end: 100.0).animate(scoreInAnimationController);tweenAnimation.addListener(() {print(tweenAnimation.value);});
/* OutputI/flutter ( 2639): 0.0I/flutter ( 2639): 0.0I/flutter ( 2639): 33.452000000000005I/flutter ( 2639): 44.602000000000004I/flutter ( 2639): 55.75133333333334I/flutter ( 2639): 66.90133333333334I/flutter ( 2639): 78.05133333333333I/flutter ( 2639): 89.20066666666668I/flutter ( 2639): 100.0*/
Tween
类生成begin
到end
之间的值,前面我们已经使用过线性的scoreInAnimationController
,相反,我们可以使用反弹曲线来获得不同的值。Tween
的优点远不止这些,你还可以补间其他东西,比如你可以补间color(颜色)
、offset(偏移量)
、position(位置)
、和其他 Widget 属性,从而进一步扩展了基础补间类。
Score Widget 位置动画
至此,我们已经掌握了足够的知识,现在可以使我们的得分Widget
在按下按钮时从底部弹出,而在离开时隐藏。
initState() {super.initState();scoreInAnimationController = new AnimationController(duration: new Duration(milliseconds: 150), vsync: this);scoreInAnimationController.addListener((){setState(() {}); // Calls render function});}
void onTapDown(TapDownDetails tap) {scoreInAnimationController.forward(from: 0.0);...
}Widget getScoreButton() {var scorePosition = scoreInAnimationController.value * 100;var scoreOpacity = scoreInAnimationController.value;return new Positioned(child: new Opacity(opacity: scoreOpacity,child: new Container(...)),bottom: scorePosition);}
如上图所示,点击按钮,Score Widget
从底部弹出了,但是这儿还有一个小问题:当多次点击按钮的时候,score widget
一次又一次的弹出,这是由于上述代码中的一个小错误。每次点击按钮时,我们都告诉控制器从 0 开始,即forward(from: 0.0)
。
score widget 退出动画
现在,我们为score Widget
添加退出动画,首先,我们添加一个枚举来更轻松地管理score Widget
的状态。
enum ScoreWidgetStatus {HIDDEN,BECOMING_VISIBLE,BECOMING_INVISIBLE}
然后,创建一个退出动画的控制器,动画控制器将使score widget
的位置从100
非线性变化到150
。我们还为动画添加了状态监听器。动画结束后,我们将得分组件
的状态设置为隐藏。
scoreOutAnimationController = new AnimationController(vsync: this, duration: duration);scoreOutPositionAnimation = new Tween(begin: 100.0, end: 150.0).animate(new CurvedAnimation(parent: scoreOutAnimationController, curve: Curves.easeOut));scoreOutPositionAnimation.addListener((){setState(() {});});scoreOutAnimationController.addStatusListener((status) {if (status == AnimationStatus.completed) {_scoreWidgetStatus = ScoreWidgetStatus.HIDDEN;}});
当用户手指离开组件的时候,我们将相应地设置状态,并启动300毫秒
的计时器。 300毫秒
后,我们将为得分组件
添加位置和不透明度动画。
void onTapUp(TapUpDetails tap) {// User removed his finger from button.scoreOutETA = new Timer(duration, () {scoreOutAnimationController.forward(from: 0.0);_scoreWidgetStatus = ScoreWidgetStatus.BECOMING_INVISIBLE;});holdTimer.cancel();}
我们还修改了onTapDown
事件以处理某些边角情况。
void onTapDown(TapDownDetails tap) {// User pressed the button. This can be a tap or a hold.if (scoreOutETA != null) scoreOutETA.cancel(); // We do not want the score to vanish!if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN) {scoreInAnimationController.forward(from: 0.0);_scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;}increment(null); // Take care of tapholdTimer = new Timer.periodic(duration, increment); // Takes care of hold}
最后,我们需要选择用于score widget
的位置和不透明度的控制器值。一个简单的开关就完成了。
Widget getScoreButton() {var scorePosition = 0.0;var scoreOpacity = 0.0;switch(_scoreWidgetStatus) {case ScoreWidgetStatus.HIDDEN:break;case ScoreWidgetStatus.BECOMING_VISIBLE :scorePosition = scoreInAnimationController.value * 100;scoreOpacity = scoreInAnimationController.value;break;case ScoreWidgetStatus.BECOMING_INVISIBLE:scorePosition = scoreOutPositionAnimation.value;scoreOpacity = 1.0 - scoreOutAnimationController.value;}return ...}
score widget
的运行效果很棒,先弹出然后逐渐消失。
Score Widget 尺寸动画
到这一步,我们几乎知道如何在分数增加时也改变大小。让我们快速添加大小动画,然后继续搞火花闪烁效果
。
我已经更新了ScoreWidgetStatus
枚举来保留一个额外的VISIBLE
值。现在,我们为size
属性添加一个新的控制器。
scoreSizeAnimationController = new AnimationController(vsync: this, duration: new Duration(milliseconds: 150));scoreSizeAnimationController.addStatusListener((status) {if(status == AnimationStatus.completed) {scoreSizeAnimationController.reverse();}});scoreSizeAnimationController.addListener((){setState(() {});});
控制器在150ms
的时间内生成从0
到1
的值,完成之后((status == AnimationStatus.completed
),又会生成从1
到0
的值。这会产生很好的增长和收缩效果。
void increment(Timer t) {scoreSizeAnimationController.forward(from: 0.0);setState(() {_counter++;});
我们需要注意处理枚举的visible
属性情况。为此,我们需要在 T??ouch down
事件中添加一些基本条件。
void onTapDown(TapDownDetails tap) {// User pressed the button. This can be a tap or a hold.if (scoreOutETA != null) {scoreOutETA.cancel(); // We do not want the score to vanish!}if(_scoreWidgetStatus == ScoreWidgetStatus.BECOMING_INVISIBLE) {// We tapped down while the widget was flying up. Need to cancel that animation.scoreOutAnimationController.stop(canceled: true);_scoreWidgetStatus = ScoreWidgetStatus.VISIBLE;}else if (_scoreWidgetStatus == ScoreWidgetStatus.HIDDEN ) {_scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;scoreInAnimationController.forward(from: 0.0);}increment(null); // Take care of tapholdTimer = new Timer.periodic(duration, increment); // Takes care of hold}
最后,我们使用Widget
中控制器的值
extraSize = scoreSizeAnimationController.value * 10;...height: 50.0 + extraSize,width: 50.0 + extraSize,...
完整的代码,可以在 github(gist.github.com/Kartik1607/… 中找到。我们同时使用大小和位置动画。大小动画需要一些调整,我们最后会介绍。
最后,火花闪烁动画
在进行火花闪烁动画
之前,我们需要对尺寸动画进行一些调整。目前,该按钮已增长太多。解决方法很简单,我们将额外的乘数从10
更改为一个较小的数字。
现在来看看火花闪烁
动画,我们可以看到到火花
其实就是位置在变化的5
张图片
我在MS Paint
中制作了一个三角形和一个圆形的图片,并将其保存为 flutter 资源。然后,我们就可以将该图片用作Image asset
。
在实现动画之前,让我们考虑一下定位以及需要完成的一些任务:
1、我们需要定位
5
个图片,每张图片以不同的角度形成一个完整的圆。2、我们需要根据角度旋转图片
3、随着时间增加圆的半径
4、需要根据角度和半径找到坐标。
简单的三角函数给了我们根据角度的正弦和余弦来获得x
和y
坐标的公式。
var sparklesWidget =new Positioned(child: new Transform.rotate(angle:
currentAngle - pi/2,child: new Opacity(opacity: sparklesOpacity,child : new Image.asset("images/sparkles.png", width: 14.0, height: 14.0, ))),left:(sparkleRadiuscos(currentAngle)) + 20,top: (sparkleRadius sin(currentAngle)) + 20 ,);
现在,我们需要创建5
个widget
。每个widget
具有不同的角度。一个简单的for
循环就 ok 了。
for(int i = 0;i < 5; ++i) {var currentAngle = (firstAngle + ((2pi)/5)(i));var sparklesWidget = ...stackChildren.add(sparklesWidget);}
将2 * pi
(360 度)分成5
个部分,并相应地创建一个widget
。然后,我们将widget
添加到stackChildren
数组中。
好了,到这一步,大多数的准备工作都做完了,我们只需要设置sparkleRadius
的动画并生成一个新的firstAngle
即可。
sparklesAnimationController = new AnimationController(vsync: this, duration: duration);sparklesAnimation = new CurvedAnimation(parent: sparklesAnimationController, curve: Curves.easeIn);sparklesAnimation.addListener((){setState(() { });});
void increment(Timer t) {sparklesAnimationController.forward(from: 0.0);...setState(() {..._sparklesAngle = random.nextDouble() * (2*pi);});Widget getScoreButton() {...
评论