本文由云+社区发表
写了几个Flutter的demo,可是对Flutter的自定义view和动画都不太了解,看到一个相似效果在android的实现,就尝试用Flutter作一下。同时也是学习Flutter的自定义view和动画相关的知识。android
效果动图canvas
在蓝色区域点击,会产品水波纹动画。ide
宛如水珠落在池塘,雨滴落在青青草地~
动画很简单,虽然有多个雨滴,不过每次点击都是重复的动画,因此只用管一个雨滴动画是怎么实现的,其余的都是重复。工具
单独来看一个雨滴动画,其实就是一个圆圈慢慢的变大同时慢慢的变浅,最后消失。性能
因此咱们封装一套上述的动画逻辑,而后在用户每次点击时生成一个相应的动画便可。学习
首先咱们要解决的是自定义view的问题,咱们知道Flutter中的一块儿UI皆Flutter,可是不一样于android中的View会直接提供一个draw方法让你作自由的绘制操做。在Flutter中,除了StatefuleWidget等申明了支持继承的类外,其余的都是不建议继承重写的。如要要作一个新的Widget,官方建议是经过组合Widget来实现。动画
固然对于咱们这里这种须要本身作绘制操做的,就不是组合能够解决的了,这种状况下,Flutter提供了CustomPainter
类,这个类提供了paint方法,能够经过重写该方法,实现对canvas的绘制。而后做为CustomPaint
的参数,控制该Widget的展现样式。this
这里因为主要的绘制是水纹,要实现多个重复动画,因此具体的绘制逻辑封装了起来spa
class RainDrop extends CustomPainter { RainDrop(this.rainList); List<RainDropDrawer> rainList = List(); // 雨点列表 Paint _paint = new Paint()..style = PaintingStyle.stroke; // 配置画笔 @override void paint(Canvas canvas, Size size) { rainList.forEach((item) { item.drawRainDrop(canvas, _paint); // 实际的绘制逻辑 }); rainList.removeWhere((item) { // 移出无效对象 return !item.isValid(); }); } // ... }
每个水纹的动画都是同样的,因此统一封装了起来。code
class RainDropDrawer { static const double MAX_RADIUS = 30; double posX; double posY; double radius = 5; RainDropDrawer(this.posX, this.posY); // (2) drawRainDrop(Canvas canvas, Paint paint) { // (1) double opt = (MAX_RADIUS - radius) / MAX_RADIUS; // (3) paint.color = Color.fromRGBO(0, 0, 0, opt); canvas.drawCircle(Offset(posX, posY), radius, paint); // (4) radius += 0.5; } bool isValid() { // (5) return radius < MAX_RADIUS; } }
注释(1)处,上文提到的CustomPainter
会把canvas传过来,在这里完成单个水纹的绘制工做。
注释(2)处,每一个水纹圈须要肯定的是位置,只要位置就好了,大小是随着时间均匀扩大的,给默认起始值就行。
注释(3)处,透明度是随着半径扩大而逐渐透明的,这里简单的作了线性的映射。
注释(4)处,绘制水纹圈,而后让水纹半径自增,实现每次绘制扩大的效果。
注释(5)处,给定失效的条件。超过必定半径这个水纹就消失了。
Flutter中提供了不少的动画实现,这里用到的是AnimationController。
其实AnimationController在这里就是提供了一个回调,每次收到vsync信号时回调作一次更新。
_animation = new AnimationController( // 由于是repeat的,这里的duration其实不care duration: const Duration(milliseconds: 200), vsync: this) ..addListener(() { if (_rainList.isEmpty) { //(1) _animation.stop(); } setState(() {}); });
这里的动画是经过repeat启动的,因此不用太关心duration,由于只要不手动关闭其实是会一直回调的。
vsync设置的是当前的widget,提供了一个ticker,会定时回调。而后在回调中setState
让当前widget更新UI。
注释(1)处是动画中止的条件判断,当每次点击往_rainList
中加一个对象,每一个对象绘制会判断大小是否有效,若是无效会被从列表中移出,当列表中没有元素时就中止动画。
上述基本实现了多个雨滴的展现和动画,而后咱们要来实现对用户点击的响应。
Flutter提供了GestureDetector
这个widget来作手势识别。因此咱们只须要用这个widget wrap住咱们的自定义view,而后实现对应的手势监听方法便可。
GestureDetector( onTapUp: (TapUpDetails tapUp) { RenderBox getBox = context.findRenderObject(); var localOffset = getBox.globalToLocal(tapUp.globalPosition); // (1) var rainDrop = RainDropDrawer(localOffset.dx, localOffset.dy); _rainList.add(rainDrop); _animation.repeat(); // (2) }, child: CustomPaint( painter: RainDrop(_rainList), ), ),
这里咱们关注用户轻点后抬起的手势,这个监听的方法会传入TapUpDetails
参数,这个参数含有抬起的位置参数,可是须要注意的是,这个坐标是全屏幕的坐标,而绘制的坐标是widget内的坐标,因此咱们须要将这个坐标转换为咱们widget内的坐标系,Flutter提供了这样的一个工具方法,参考注释(1)
处的实现便可。
完成了坐标换算,就能够构建一个“雨点”对象,添加到List里面。而后在注释(2)
处启动动画,就能够看到咱们文章开头的动画效果啦~
Flutter的动画实现起来真的很简单,提供一个差值回调,而后不停的更新便可。不过这里暂时没有考虑性能等问题,对setState
这个方法感受仍是很黑盒,不太懂Flutter具体的UI刷新原理。
后面会梳理一下这类原理知识,不然仍是有点担心复杂动画按这种写法是否会卡顿。
此文已由做者受权腾讯云+社区发布