教你如何使用 Jetpack 绘制天气图,史上最详细!,跨平台 app 开发框架
fun rainDrop() {
//循环播放的动画 ( 0f ~ 1f)
val animateTween by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
tween(durationMillis, easing = LinearEasing),
RepeatMode.Restart //start 动画
)
)
Canvas(modifier) {
// scope : 绘制区域
val width = size.width
val x: Float = size.width / 2
// width/2 是 strokCap 的宽度,scopeHeight 处预留 strokCap 宽度,让雨滴移出时保持正圆,提高视觉效果
val scopeHeight = size.height - width / 2
// space : 两线段的间隙
val space = size.height / 2.2f + width / 2 //间隙 size
val spacePos = scopeHeight * animateTween //锚点位置随 animationState 变化
val sy1 = spacePos - space / 2
val sy2 = spacePos + space / 2
// line length
val lineHeight = scopeHeight - space
// line1
val line1y1 = max(0f, sy1 - lineHeight)
val line1y2 = max(line1y1, sy1)
// line2
val line2y1 = min(sy2, scopeHeight)
val line2y2 = min(line2y1 + lineHeight, scopeHeight)
// draw
drawLine(
Color.Black,
Offset(x, line1y1),
Offset(x, line1y2),
strokeWidth = width,
colorFilter = ColorFilter.tint(
Color.Black
),
cap = StrokeCap.Round
)
drawLine(
Color.Black,
Offset(x, line2y1),
Offset(x, line2y2),
strokeWidth = width,
colorFilter = ColorFilter.tint(
Color.Black
),
cap = StrokeCap.Round
)
}
}
上面完成了单个雨滴的图形和动画,接下来我们使用三个雨滴组成雨水的效果。
首先可以使用 Row+Space 的方式进行组装,但是这种方式缺少灵活性,仅通过 Modifier 很难准确布局三个雨滴的相对位置。因此考虑转而使用 Compose 的自定义布局,以提高灵活性和准确性:
Layout(
modifier = modifier.rotate(30f), //雨滴旋转角度
content = { // 定义子 Composable
Raindrop(modifier.fillMaxSize())
Raindrop(modifier.fillMaxSize())
Raindrop(modifier.fillMaxSize())
}
) { measurables, constraints ->
// List of measured children
val placeables = measurables.mapIndexed { index, measurable ->
// Measure each children
val height = when (index) { //让三个雨滴的 height 不同,增加错落感
0 -> constraints.maxHeight * 0.8f
1 -> constraints.maxHeight * 0.9f
2 -> constraints.maxHeight * 0.6f
else -> 0f
}
measurable.measure(
constraints.copy(
minWidth = 0,
minHeight = 0,
maxWidth = constraints.maxWidth / 10, // raindrop width
maxHeight = height.toInt(),
)
)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = constraints.maxWidth / ((placeables.size + 1) * 2)
// Place children in the parent layout
placeables.forEachIndexed { index, placeable ->
// Position item on the screen
placeable.place(x = xPosition, y = 0)
// Record the y co-ord placed up to
xPosition += (constraints.maxWidth / ((placeables.size + 1) * 0.8f)).roundToInt()
}
}
}
Compose 中,可以通过 Layout{…}对 Composable 进行自定义布局,content{…}中定义参与布局的子 Composable。
跟传统 Android 视图一样,自定义布局需要先后经历 measure、layout 两步。
measrue: measurables 返回所有待测量的子 Composable,constraints 类似于 MeasureSpec,封装父容器对子元素的布局约束。measurable.measure()中对子元素进行测量
layout:placeables 返回测量后的子元素,依次调用 placeable.place()对雨滴进行布局,通过 xPosition 预留雨滴在 x 轴的间隔
经过 layout 之后,通过 modifier.rotate(30f) 对 Composable 进行旋转,完成最终效果:
=================================================================
雪天效果的关键在于雪花的飘落。
雪花的绘制非常简单,用一个圆圈代表一个雪花
Canvas(modifier) {
val radius = size / 2
drawCircle( //白色填充
color = Color.White,
radius = radius,
style = FILL
)
drawCircle(// 黑色边框
color = Color.Black,
radius = radius,
style = Stroke(width = radius * 0.5f)
)
}
雪花飘落的过程相对于雨滴坠落要复杂一些,由三个动画组成:
下降:通过改变 y 轴位置实现 (0f ~ 2.5f)
左右飘移:通过该表 x 轴的 offset 实现 (-1f ~ 1f)
逐渐消失:通过改变 alpha 实现(1f ~ 0f)
借助 InfiniteTransition 同步控制多个动画,代码如下:
@Composable
private fun Snowdrop(
modifier: Modifier = Modifier,
durationMillis: Int = 1000 // 雪花飘落动画的 druation
) {
//循环播放的 Transition
val transition = rememberInfiniteTransition()
//1. 下降动画:restart 动画
val animateY by transition.animateFloat(
initialValue = 0f,
targetValue = 2.5f,
animationSpec = infiniteRepeatable(
tween(durationMillis, easing = LinearEasing),
RepeatMode.Restart
)
)
//2. 左右飘移:reverse 动画
val animateX by transition.animateFloat(
initialValue = -1f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
tween(durationMillis / 3, easing = LinearEasing),
RepeatMode.Reverse
)
)
//3. alpha 值:restart 动画,以 0f 结束
val animateAlpha by transition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
tween(durationMillis, easing = FastOutSlowInEasing),
)
)
Canvas(modifier) {
val radius = size.width / 2
// 圆心位置随 AnimationState 改变,实现雪花飘落的效果
val _center = center.copy(
x = center.x + center.x * animateX,
y = center.y + center.y * animateY
)
drawCircle(
color = Color.White.copy(alpha = animateAlpha),//alpha 值的变化实现雪花消失效果
center = _center,
radius = radius,
)
drawCircle(
color = Color.Black.copy(alpha = animateAlpha),
center = _center,
radius = radius,
style = Stroke(width = radius * 0.5f)
)
}
}
animateY 的 targetValue 设为 2.5f,让雪花的运动轨迹更长,看起来更加真实
像雨滴一样,对雪花也使用 Layout 自定义布局
@Composable
fun Snow(
modifier: Modifier = Modifier,
animate: Boolean = false,
) {
Layout(
modifier = modifier,
content = {
//摆放三个雪花,分别设置不同 duration,增加随机性
Snowdrop( modifier.fillMaxSize(), 2200)
Snowdrop( modifier.fillMaxSize(), 1600)
Snowdrop( modifier.fillMaxSize(), 1800)
}
) { measurables, constraints ->
val placeables = measurables.mapIndexed { index, measurable ->
val height = when (index) {
// 雪花的 height 不同,也是为了增加随机性
0 -> constraints.maxHeight * 0.6f
1 -> constraints.maxHeight * 1.0f
2 -> constraints.maxHeight * 0.7f
else -> 0f
}
measurable.measure(
constraints.copy(
minWidth = 0,
minHeight = 0,
maxWidth = constraints.maxWidth / 5, // snowdrop width
maxHeight = height.roundToInt(),
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = constraints.maxWidth / ((placeables.size + 1))
placeables.forEachIndexed { index, placeable ->
placeable.place(x = xPosition, y = -(constraints.maxHeight * 0.2).roundToInt())
xPosition += (constraints.maxWidth / ((placeables.size + 1) * 0.9f)).roundToInt()
}
}
}
}
最终效果如下:
=================================================================
通过一个旋转的太阳代表晴天效果
太阳的图形由中间的圆形和围绕圆环的等分竖线组成。
@Composable
fun Sun(modifier: Modifier = Modifier) {
Canvas(modifier) {
val radius = size.width / 6
val stroke = size.width / 20
// draw circle
drawCircle(
color = Color.Black,
radius = radius + stroke / 2,
style = Stroke(width = stroke),
)
drawCircle(
color = Color.White,
radius = radius,
style = Fill,
)
// draw line
val lineLength = radius * 0.2f
val lineOffset = radius * 1.8f
(0..7).forEach { i ->
val radians = Math.toRadians(i * 45.0)
val offsetX = lineOffset * cos(radians).toFloat()
val offsetY = lineOffset * sin(radians).toFloat()
val x1 = size.width / 2 + offsetX
val x2 = x1 + lineLength * cos(radians).toFloat()
val y1 = size.height / 2 + offsetY
val y2 = y1 + lineLength * sin(radians).toFloat()
drawLine(
color = Color.Black,
start = Offset(x1, y1),
end = Offset(x2, y2),
strokeWidth = stroke,
cap = StrokeCap.Round
)
}
}
}
均分 360 度,每间隔 45 度画一条竖线,cos 计算 x 轴坐标,sin 计算 y 轴坐标。
太阳的旋转动画很简单,通过 Modifier.rotate 不断转动 Canvas 即可。
@Composable
fun Sun(modifier: Modifier = Modifier) {
//循环动画
val animateTween by rememberInfiniteTransition().animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(tween(5000), RepeatMode.Restart)
)
Canvas(modifier.rotate(animateTween)) {// 旋转动画
val radius = size.width / 6
val stroke = size.width / 20
val centerOffset = Offset(size.width / 30, size.width / 30) //圆心偏移量
// draw circle
drawCircle(
color = Color.Black,
radius = radius + stroke / 2,
style = Stroke(width = stroke),
center = center + centerOffset //圆心偏移
)
//...略
}
}
此外,DrawScope 也提供了 rotate 的 API,也可以实现旋转效果。
最后我们给太阳的圆心增加一个偏移量,让转动更加活泼:
=====================================================================
上面分别实现了 Rain、Snow、Sun 等图形,接下来使用这些元素组合成各种天气效果。
Compose 的声明式语法非常有利于 UI 的组合:
比如,多云转阵雨,我们摆放 Sun、Cloud、Rain 等元素后,通过 Modifier 调整各自位置即可:
@Composable
fun CloudyRain(modifier: Modifier) {
Box(modifier.size(200.dp)){
Sun(Modifier.size(120.dp).offset(140.dp, 40.dp))
Rain(Modifier.size(80.dp).offset(80.dp, 60.dp))
Cloud(Modifier.align(Aligment.Center))
}
}
复制代码
当在多个天气动画之间进行切换时,我们希望能实现更自然的过渡。实现思路是将组成天气动画的各元素的 Modifier 信息变量化,然后通过 Animation 进行改变 state
假设所有的天气都可以由 Cloud、Sun、Rain 组合而成,无非就是 offset、size、alpha 值的不同:
data class IconInfo(
val size: Float = 1f,
val offset: Offset = Offset(0f, 0f),
val alpha: Float = 1f,
)
复制代码
//天气组合信息,即 Sun、Cloud、Rain 的位置信息
data class ComposeInfo(
val sun: IconInfo,
val cloud: IconInfo,
val rains: IconInfo,
) {
operator fun times(float: Float): ComposeInfo =
copy(
sun = sun * float,
cloud = cloud * float,
rains = rains * float
)
operator fun minus(composeInfo: ComposeInfo): ComposeInfo =
copy(
评论