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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: ‘Flutter Demo’,
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: ‘Flutter Demo Home Page’),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;

@override
_MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
new Text(
‘You have pushed the button this many times:’,
),
new Text(
‘$_counter’,
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: ‘Increment’,
child: new Icon(Icons.add),
),
);
}
}

Flutter为我们提供了一些免费的入门代码,它为我们创建了一个浮动操作按钮,并且自动帮我们管理计数的状态。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下图是我们最终要实现的效果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在添加动画之前,让我们快速解决一些简单的问题:

  • 更改按钮图标和背景。

  • 按住按钮时,按钮应继续增加计数。

让我们快速解决上面2个问题,然后开始实现动画:

class _MyHomePageState extends State {
int _counter = 0;
final duration = new Duration(milliseconds: 300);
Timer timer;

initState() {
super.initState();
}

dispose() {
super.dispose();
}

void increment(Timer t) {
setState(() {
_counter++;
});
}

void onTapDown(TapDownDetails tap) {
// User pressed the button. This can be a tap or a hold.
increment(null); // Take care of tap
timer = new Timer.periodic(duration, increment); // Takes care of hold
}

void onTapUp(TapUpDetails tap) {
// User removed his finger from button.
timer.cancel();
}

Widget getScoreButton() {

return new Positioned(
child: new Opacity(opacity: 1.0, child: new Container(
height: 50.0 ,
width: 50.0 ,
decoration: new ShapeDecoration(
shape: new CircleBorder(
side: BorderSide.none
),
color: Colors.pink,
),
child: new Center(child:
new Text(“+” + _counter.toString(),
style: new TextStyle(color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15.0),))
)),
bottom: 100.0
);
}

Widget getClapButton() {
// Using custom gesture detector because we want to keep increasing the claps
// when user holds the button.
return new GestureDetector(
onTapUp: onTapUp,
onTapDown: onTapDown,
child: new Container(
height: 60.0 ,
width: 60.0 ,
padding: new EdgeInsets.all(10.0),
decoration: new BoxDecoration(
border: new Border.all(color: Colors.pink, width: 1.0),
borderRadius: new BorderRadius.circular(50.0),
color: Colors.white,
boxShadow: [
new BoxShadow(color: Colors.pink, blurRadius: 8.0)
]
),
child: new ImageIcon(
new AssetImage(“images/clap.png”), color: Colors.pink,
size: 40.0),
)
);
}

@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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: [
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);
/* OUTPUT
I/flutter ( 1913): 0.0
I/flutter ( 1913): 0.0
I/flutter ( 1913): 0.22297333333333333
I/flutter ( 1913): 0.3344533333333333
I/flutter ( 1913): 0.4459333333333334
I/flutter ( 1913): 0.5574133333333334
I/flutter ( 1913): 0.6688933333333335
I/flutter ( 1913): 0.7803666666666668
I/flutter ( 1913): 0.8918466666666668
I/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);
});

/*OUTPUT
I/flutter ( 5221): 0.0
I/flutter ( 5221): 0.0
I/flutter ( 5221): 0.24945376519722218
I/flutter ( 5221): 0.16975716286388898
I/flutter ( 5221): 0.17177866222222238
I/flutter ( 5221): 0.6359024059750003
I/flutter ( 5221): 0.9119433941222221
I/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);
});

/* Output
I/flutter ( 2639): 0.0
I/flutter ( 2639): 0.0
I/flutter ( 2639): 33.452000000000005
I/flutter ( 2639): 44.602000000000004
I/flutter ( 2639): 55.75133333333334
I/flutter ( 2639): 66.90133333333334
I/flutter ( 2639): 78.05133333333333
I/flutter ( 2639): 89.20066666666668
I/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;

总结

找工作是个很辛苦的事情,而且一般周期都比较长,有时候既看个人技术,也看运气。第一次找工作,最后的结果虽然不尽如人意,不过收获远比offer大。接下来就是针对自己的不足,好好努力了。

最后为了节约大家的时间,我把我学习所用的资料和面试遇到的问题和答案都整理成了PDF文档

喜欢文章的话请关注、点赞、转发 谢谢!


《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
onController.forward(from: 0.0);
_scoreWidgetStatus = ScoreWidgetStatus.BECOMING_VISIBLE;

总结

找工作是个很辛苦的事情,而且一般周期都比较长,有时候既看个人技术,也看运气。第一次找工作,最后的结果虽然不尽如人意,不过收获远比offer大。接下来就是针对自己的不足,好好努力了。

最后为了节约大家的时间,我把我学习所用的资料和面试遇到的问题和答案都整理成了PDF文档

喜欢文章的话请关注、点赞、转发 谢谢!

[外链图片转存中…(img-ThEYoxD2-1714964209875)]
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 25
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值