中秋佳节:Compose 实现月亮阴晴圆缺动画
效果图
“人有悲欢离合,月有阴晴圆缺,此事古难全。
”
但愿人长久,千里共婵娟。
恰逢中秋佳节,我们今天就使用Compose
来实现一下月相变化动画吧~
感兴趣的同学可以点个Star
: Compose 实现月亮阴晴圆缺动画
主要思路
满天繁星
为了实现月相动画,我们首先需要一个背景,因此我们需要一个好看的星空,最好还有闪烁的效果
为为实现星空背景,我们需要做以下几件事
绘制背景 生成几十个星星,在背景上随机分布 通过 scale
与alpha
动画,实现每个星星的闪烁效果
我们一起来看下代码
@Composable
fun Stars(starNum: Int) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val list = remember { mutableStateListOf<Star>() }
LaunchedEffect(true) {
for (i in 0..starNum) {
delay(100L)
//添加星星,它们的位置在屏幕上随机
list.add(Star(maxWidth.value * density, maxHeight.value * density))
}
}
list.forEach {
Star(it)
}
}
}
@Composable
fun Star(star: Star) {
var progress: Float by remember { mutableStateOf(0f) }
val infiniteTransition = rememberInfiniteTransition()
....
star.updateStar(progress) // 通过动画更新progress,从而更新star的属性值
Canvas(modifier = Modifier.wrapContentSize()) {
scale(star.scale, Offset(star.x, star.y)) { // 缩放动画
drawCircle(
star.starColor,
star.radius,
center = Offset(star.x, star.y),
alpha = star.alpha // alpha动画
)
}
}
}
月相变化
月相,天文学术语。(phase of the moon
)是天文学中对于地球上看到的月球被太阳照明部分的称呼。随着月亮每天在星空中自东向西移动一大段距离,它的形状也在不断地变化着,这就是月亮位相变化,叫做月相。
它的变化过程如下图所示
每个阶段都有各自的名字,如下图所示:
可以看出,月相变化过程还是有些复杂的,那我们怎么实现这个效果呢?
思路分析
为了实现月相变化,首先我们需要画一个圆,代表月亮,最终的满月其实就是这样,比较简单
有了满月,如何在它的基础上,画出其它的月相呢?我们可以通过图像混合模式来实现
图像混合模式定义的是,当两个图像合成时,图像最终的展示方式。在Androd
中,有相应的API
接口来支持图像混合模式,即Xfermode
.
图像混合模式主要有以下16种,以下这张图片从一定程度上形象地说明了图像混合的作用,两个图形一圆一方通过一定的计算产生不同的组合效果,具体如下
我们为了实现月相动画,主要需要使用以下两种混合模式
DST_OUT
:只在源图像和目标图像不相交的地方绘制【目标图像】,在相交的地方根据源图像的alpha
进行过滤,源图像完全不透明则完全过滤,完全透明则不过滤DST_OVER
:将目标图像放在源图像上方
我们已经了解了图形混合模式,那么需要在满月上画什么才能实现其它效果呢?
我们可以通过在满月上放一个半圆+
一个椭圆来实现
如上所示,椭圆上水平的线叫长轴,竖直的线叫短轴 短轴不变,长轴半径从0到满月半径发生变化,再加上一个半圆,就可以实现不同的月相 比如为了画上蛾眉月,可以通过左半边画半圆,再加上一个椭圆,两都都使用 DST_OVER
混合模式来实现,就实现了它们两的并集,然后覆盖在下层满月上,就实现了上蛾眉月为了画渐盈凸月,则同样就左半边以 DST_OVER
画半圆,再以DST_OUT
画椭圆,就只剩下半圆与椭圆不相交的部分,再与下层的满月混合,就实现了渐盈凸月
这样说可能还是比较抽象,感兴趣的同学可下载源码详细了解下
源码实现
//月亮动画控件
@Composable
fun Moon(modifier: Modifier) {
var progress: Float by remember { mutableStateOf(0f) }
BoxWithConstraints(modifier = modifier) {
Canvas(
modifier = Modifier
.size(canvasSize)
.align(Alignment.TopCenter)
) {
drawMoonCircle(this, progress)
drawIntoCanvas {
it.withSaveLayer(Rect(0f, 0f, size.width, size.height), paint = Paint()) {
if (progress != 1f) {
//必须先画半圆,再画椭圆
drawMoonArc(this, it, paint, progress)
drawMoonOval(this, it, paint, progress)
}
}
}
}
}
}
// 1.首先画一个满月
private fun drawMoonCircle(scope: DrawScope, progress: Float) {
//....
drawCircle(Color(0xfff9dc60))
}
// 2. 画半圆
private fun drawMoonArc(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) {
val sweepAngle = when { //从新月到满月在一边画半圆,从满月回到新月则在另一边画半圆
progress <= 0.5f -> 180f
progress <= 1f -> 180f
progress <= 1.5f -> -180f
else -> -180f
}
paint.blendMode = BlendMode.DstOver //半圆的混合模式始终是DstOver
scope.run {
canvas.drawArc(Rect(0f, 0f, size.width, size.height), 90f, sweepAngle, false, paint)
}
}
// 3. 画椭圆
private fun drawMoonOval(scope: DrawScope, canvas: Canvas, paint: Paint, progress: Float) {
val blendMode = when { //椭圆的混合模式会发生变化,这里需要注意下
progress <= 0.5f -> BlendMode.DstOver
progress <= 1f -> BlendMode.DstOut
progress <= 1.5f -> BlendMode.DstOut
else -> BlendMode.DstOver
}
paint.blendMode = blendMode
scope.run {
canvas.drawOval(
Rect(offset = topLeft, size = Size(horizontalAxis, verticalAxis)), //椭圆的长轴会随着动画变化
paint = paint
)
}
}
如上所示:
主要就是3个步骤,画满月,再画半圆,再画椭圆 半圆的混合模式始终是 DstOver
,而椭圆的混合模式会发生变化,它们的颜色都是黑色。可以看到半圆与椭圆新建了一个 Layer
,混合模式的变化,表示的就是最后剩下的是它们的并集,还是Dst
不相交的部分,最后覆盖到满月上,所以必须先画半圆随着动画的变化,椭圆的长轴会发生变化,这样就可以实现不同的月相
诗歌打字机效果
上面其实已经做得差不多了,我们最后再添加一些诗歌,并为它们添加打字机效果
@Composable
fun PoetryColumn(
list: List<Char>,
offsetX: Float = 0f,
offsetY: Float = 0f
) {
val targetList = remember { mutableStateListOf<Char>() }
LaunchedEffect(list) {
targetList.clear()
list.forEach {
delay(500) //通过在LaunchedEffect中delay实现动画效果
targetList.add(it)
}
}
//将 Jetpack Compose 环境的 Paint 对象转换为原生的 Paint 对象
val textPaint = Paint().asFrameworkPaint().apply {
//...
}
Canvas(modifier = Modifier.wrapContentSize()) {
drawIntoCanvas {
for (i in targetList.indices) {
it.nativeCanvas.drawText(list[i].toString(), x, y, textPaint)
y += delta // 更新文字y轴位置
}
}
}
}
如上所示,代码比较简单
通过在 LaunchedEffect
中调用挂起函数,来实现动画效果为了实现竖直方向的文字,我们需要使用 Paint
来绘制Text
,而不能使用Text
组件Compose
目前还不支持直接绘制Text
,所以我们需要调用asFrameworkPaint
将其转化为原生的Paint
总结
通过以上步骤,我们就通过Compose
实现了月相阴晴圆缺+星空闪耀+诗歌打字机的动画效果
开发起来跟Android
自定义绘制其实并没有多大差别,代码量因为Compose
强大的API
与声明式特点可能还有所减少
在我看来,Compose
已经相当成熟了,而且将是Android UI
的未来~
开源不易,如果项目对你有所帮助,欢迎点赞,Star
,收藏~
参考资料
蹭中秋热度来了~Android 自定义View——月有阴晴圆缺
「寒草的中秋献礼🥮,实现30s前端创意动画」陪你看日落和月升|与你赏星空和诗歌
项目地址
Compose 实现月亮阴晴圆缺动画