写点什么

Flutter 动画:用 Flutter 来实现一个拍手动画

用户头像
Android架构
关注
发布于: 21 小时前

]),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.01.0的数字。请注意,生成的值几乎是线性的。 0.20.30.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.01.0的值。曲线动画Widget根据我们设置的曲线对这些值进行插值。


尽管我们得到了0.01.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类生成beginend之间的值,前面我们已经使用过线性的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的时间内生成从01的值,完成之后((status == AnimationStatus.completed),又会生成从10的值。这会产生很好的增长和收缩效果。


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、需要根据角度和半径找到坐标。


简单的三角函数给了我们根据角度的正弦和余弦来获得xy坐标的公式。


var sparklesWidget =new Positioned(child: new Transform.rotate(angle:


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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 ,);


现在,我们需要创建5widget。每个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() {...

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Flutter动画:用Flutter来实现一个拍手动画