共计 2899 个字符,预计需要花费 8 分钟才能阅读完成。
本文由云 + 社区发表
目的
写了几个 Flutter 的 demo,但是对 Flutter 的自定义 view 和动画都不太了解,看到一个类似效果在 android 的实现,就尝试用 Flutter 做一下。同时也是学习 Flutter 的自定义 view 和动画相关的知识。
效果
效果动图
在蓝色区域点击,会产品水波纹动画。
宛如水珠落在池塘,雨滴落在青青草地~
思路
动画很简单,虽然有多个雨滴,不过每次点击都是重复的动画,所以只用管一个雨滴动画是怎么实现的,其他的都是重复。
单独来看一个雨滴动画,其实就是一个圆圈慢慢的变大同时慢慢的变浅,最后消失。
所以我们封装一套上述的动画逻辑,然后在用户每次点击时生成一个相应的动画即可。
实现
自定义 view
首先我们要解决的是自定义 view 的问题,我们知道 Flutter 中的一起 UI 皆 Flutter,但是不同于 android 中的 View 会直接提供一个 draw 方法让你做自由的绘制操作。在 Flutter 中,除了 StatefuleWidget 等申明了支持继承的类外,其他的都是不建议继承重写的。如要要做一个新的 Widget,官方建议是通过组合 Widget 来实现。
当然对于我们这里这种需要自己做绘制操作的,就不是组合可以解决的了,这种情况下,Flutter 提供了 CustomPainter 类,这个类提供了 paint 方法,可以通过重写该方法,实现对 canvas 的绘制。然后作为 CustomPaint 的参数,控制该 Widget 的展示样式。
这里由于主要的绘制是水纹,要实现多个重复动画,所以具体的绘制逻辑封装了起来
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();
});
}
// …
}
水纹圈的绘制
每一个水纹的动画都是一样的,所以统一封装了起来。
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 刷新原理。
后面会梳理一下这类原理知识,否则还是有点担忧复杂动画按这种写法是否会卡顿。
此文已由作者授权腾讯云 + 社区发布