写点什么

教你如何使用 Jetpack 绘制天气图,史上最详细!,跨平台 app 开发框架

用户头像
Android架构
关注
发布于: 1 小时前

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


)


}


}


Compose 自定义布局




上面完成了单个雨滴的图形和动画,接下来我们使用三个雨滴组成雨水的效果。


首先可以使用 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 进行旋转,完成最终效果:



5. 雪天效果


=================================================================


雪天效果的关键在于雪花的飘落。



雪花的绘制




雪花的绘制非常简单,用一个圆圈代表一个雪花


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 = {


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


//摆放三个雪花,分别设置不同 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()


}


}


}


}


最终效果如下:



6. 晴天效果


=================================================================


通过一个旋转的太阳代表晴天效果



太阳的绘制




太阳的图形由中间的圆形和围绕圆环的等分竖线组成。


@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,也可以实现旋转效果。


最后我们给太阳的圆心增加一个偏移量,让转动更加活泼:



7. 动画的组合、切换


=====================================================================


上面分别实现了 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 值的不同:


ComposeInfo




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(

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
教你如何使用Jetpack绘制天气图,史上最详细!,跨平台app开发框架