开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情。
缘起
2.14 是众所周知的节日,很显然,与我无关,甚至垃圾桶里的二手玫瑰都被大佬们抢啦😅。
但是!!!我去看了流浪地球2,有一说一,国产科幻起飞!电影里未来程序员都失业啦,一个量子计算机都能实时生成系统啦(更别说程序了)。正在我担心300年后的程序员孙辈们还能不能端起咱们得饭碗时,看到了这个(剧透警告):
咱地球的唯一卫星--月球被 big mama炸了。
所以,小小安卓写一个赛博月球以纪念她!
开始
这是鸿蒙的引力加载动效果,一个小卫星绕着一个上下移动的主星在公转,似乎很简单:
Step 1 : 绘制主星
Compose 给我们提供了丰富的 canvas 绘制 api ,绘制一个圆不多说:
Canvas(
modifier = Modifier
.align(Alignment.Center)
) {
drawCircle(
color = Color.Gray, radius = 50f, style = Stroke(width = 10f)
)
}
然后给主星增加上下移动动画,我们需要使用无限循环动画来实现,使用 infinitRepeatable 轻轻松松:
val infiniteTransition = rememberInfiniteTransition()
val offsetY by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 5f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1500
0f at 0
10f at 725
0f at 1500
}
)
)
Canvas(
modifier = Modifier
.align(Alignment.Center)
.offset(y = LocalDensity.current.run { offsetY.toDp() }) // 使用动画
) {
...
}
看看效果:
Step 2 : 绘制卫星
观察原型图,可以发现卫星的运动轨迹是围绕一个椭圆进行旋转往复运动,这样在视觉上可以以后立体的感觉。如图:
使用 Path 描述椭圆路径
如果需要让目标沿着一个特殊的轨迹进行运动的话,使用 path 动画还是比较方便的。
Canvas(
modifier = Modifier.align(Alignment.Center)
) {
val path = Path()
path.addOval(Rect(-75f, -35f, 75f, 35f))
rotate(-20f) {
// *坑点1,需要旋转画布来实现斜向椭圆
}
}
}
}
坑点 1 出现 :绘制 api 只能绘制横着的或者竖着的椭圆(椭圆的两个定点在x/y轴上),不能绘制斜的,我们需要通过几何变换来实现
在椭圆 path 上绘制卫星
现在我们已经有了斜向椭圆的 path , 接着通过一个神奇的工具PathMeasure,我们可以的到 path 的有用信息。
pathMeasure 的 getPosTan 方法是用来获取坐标点的,我们提供两个数组分别保存pos 和 tan。
其中pos参数是获取Path指定distance位置的坐标点,也是就指图中的A点,而tan参数是指圆心点:
val animatePer by infiniteTransition.animateFloat(initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(animation = keyframes {
durationMillis = 1300
0f at 0
1f at 1300
}
))
val pathMeasure = android.graphics.PathMeasure()
val pos = FloatArray(2)
val tan = FloatArray(2)
path.addOval(Rect(-75f, -35f, 75f, 35f))
rotate(-20f) {
pathMeasure.setPath(path.asAndroidPath(), true) // * 坑点2 compose下PathMeasure并没有获取位置的方法
pathMeasure.getPosTan(pathMeasure.length * animatePer, pos, tan)
...
}
坑点 2 :由于在compose下PathMeasure并没有获取位置的方法,所以我们使用android.graphics.PathMeasure(),然后path也需要转换成 android.graphics.Path
同时,我们准备一个控制进度的动画,使用pathMeasure.length * animatePer动态获取到整个path的所有点的坐标。
尝试1:drawPoint来实现
在我们已经得到path上得所有坐标之后,我们将坐标变成一个drawpoint的坐标来绘制出来,compose的drawpoint比较麻烦,这里我们使用原生的canvas来绘制更加方便:
val paint = Paint().asFrameworkPaint().apply {
isAntiAlias = true
strokeWidth = 25f
color = android.graphics.Color.GRAY
strokeCap = android.graphics.Paint.Cap.ROUND
style = android.graphics.Paint.Style.STROKE
}
// 通过 drawIntoCanvas可以获取原生的 canvas
drawIntoCanvas {
it.nativeCanvas.drawPoint(pos[0], pos[1], paint)
}
可以看到,已经有个模样啦😎,但是仔细观察原素材,可以发现卫星并不是一个简单的点,而且有一个拖尾效果:
尝试2:drawArc实现
我们可以沿着path绘制一段弧形,并给弧形增加渐变来模拟卫星的拖尾效果,同样的这里用到了原生的canvas api ( 用compose的也可以 )。
另外为了避免内存抖动,我们使用 drawWithCache 将 path 对象的创建放在其中
有些时候我们绘制一些比较复杂的UI效果时,不希望当 Recompose 发生时所有绘画所用的所有实例都重新构建一次(类似Path),这可能会产生内存抖动。在 Compose 中我们一般能够想到使用 remember 进行缓存,然而我们所绘制的作用域是 DrawScope 并不是 Composable,所以无法使用 remember,那我们该怎么办呢?drawWithCache 提供了这个能力。--- 摘录与社区大佬们维护的 compose 博物馆
val paint = Paint().asFrameworkPaint()
Spacer(modifier = Modifier
.align(Alignment.Center)
.drawWithCache {
val path = Path()
val pathMeasure = android.graphics.PathMeasure()
val pos = FloatArray(2)
val tan = FloatArray(2)
onDrawBehind {
path.addOval(Rect(-75f, -35f, 75f, 35f))
rotate(-20f) {
pathMeasure.setPath(path.asAndroidPath(), true)
pathMeasure.getPosTan(pathMeasure.length * animatePer, pos, tan)
paint.apply {
isAntiAlias = true
strokeWidth = 25f
strokeCap = android.graphics.Paint.Cap.ROUND
style = android.graphics.Paint.Style.STROKE
shader = android.graphics.RadialGradient(
pos[0],
pos[1],
80f,
android.graphics.Color.parseColor("#666466"),
android.graphics.Color.parseColor("#e5e3e5"),
Shader.TileMode.CLAMP
)
}
drawIntoCanvas {
it.nativeCanvas.drawArc(
RectF(-80f, -20f, 80f, 20f),
(atan2(pos[1], pos[0])) * 180 / PI.toFloat(),
20f,
false,
paint
)
}
}
}
})
坑点 3 :绘制圆弧需要用到角度知识,因为我们已经通过pathMeasure得到了椭圆上的点的x,y,所以我们可以通过反三角函数获取 startAngle,并且注意需要使用 atan2 而不是 atan
完成,看看效果!
总结
原本是工作完成之后,无聊找个玩具练练手,但是实际上手之后,才发现实现过程并没有自己想象的那么简单,本来打算1~2h 完成之后等下班,结果下班的时候才完成了drawpoint 那个版本,直到第二天又敲了几个小时才实现,并且效果也差强人意。所以有些时候还是不要想当然觉得So easy,还是需要认真去编码。