本文示例代码API基于compose UI 1.0.0-bate08
图形
官网说明
官网说明比较简单,一共2个Canvas和DrawScope。
Canvas
Canvas 是一个封装过的对象,点开源码其实就是一个Spacer:
@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
Spacer(modifier.drawBehind(onDraw))
Canvas 封装了Spacer在它的约束条件上drawBehind(),入参就是Canvas的 DrawScope.() -> Unit。
引申一下,Modifier接口都可以drawBehind(DrawScope.() -> Unit),Canvas用Spacer封装是因为Spacer是一个无布局的Composable。所以,我们可以在任何的Modifier接口后面使用drawBehind函数,达到绘制背景的效果。比如: Text
Text(text = "markrenChina",modifier = Modifier
.size(100.dp)
.drawBehind {
drawRect(color = Color(0xFF00FF00))
})
drawRect可能跟Modifier.background没什么区别,大家可以换成drawImage,drawCircle 等,或者换成一个复杂的背景,想象空间非常大。
DrawScope
这个是图形的关键,androidx.compose.ui.graphics.drawscope包下提供了很多已经封装过的函数,如果有java Canvas Painter等知识,上手会比较快一点。自学java时写的一个游戏,全程painter画面
我们举例一个自定义背景,屏幕一点是长方形,所以用drawRect,Brush 参数,背景我们用三个颜色渐变(orange,lightPink,lightPurple):
//去掉了Canvas封装,如果全屏背景,不要放在任何布局下面
Spacer(
Modifier
.fillMaxSize()
.drawBehind {
val brushBackground = Brush.verticalGradient(
listOf(orange, lightPink, lightPurple),
0f,
size.height.toDp().toPx(),
TileMode.Mirror
)
drawRect(brush = brushBackground)
})
效果图:
更复杂的效果我们结合动画来讲。
动画
官网示例要像看效果,举其中Animatable示例,我们可以这样:
var ok by remember { mutableStateOf(false) }
val color = remember { Animatable(Color.Gray) }
//利用协程延时2秒,死循环改变
LaunchedEffect(ok) {
color.animateTo(if (ok) Color.Green else Color.Red)
delay(2000)
ok = !ok
}
Box(
Modifier
.size(100.dp)
.background(color.value)
)
自定义动画
通过自定义animate*AsState 的AnimationSpec来实现。
具体看官方介绍,简单举例,我们用infiniteRepeatable来改写上面的举例,同样达到死循环的效果,并且整加了动画渐变。
val infiniteTransition = rememberInfiniteTransition()
val value by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 2000,easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Box(
Modifier
.size(100.dp)
.background(value)
)
结合Canvas ,我们作一些有趣的动画,比如这样:
非实心圆,我们用到DrawScope的drawArc() 这个函数有一句说明很重要@param startAngle Starting angle in degrees. 0 represents 3 o’clock,起始位置是3点钟方向,绘制方式是drawn clockwise (顺时针)。
首先,我们先用自定义协程(比较好理解),我们需要一个progress进度状态,一个协程去改变这个状态。然后Canvas把自定义圆圈画出来。因为本身是顺时针效果做逆时针,需要从满圈往0画。
var progress by remember { mutableStateOf(360f) }
LaunchedEffect(key1 = progress) {
//死循环放开注释
//if (progress <= 0F) { progress = 360F }
delay(100)
progress -= 1F
}
Canvas(
modifier = Modifier
.padding(80.dp)
.size(180.dp)
) {
// 背景图示中的淡黑色,如果没有,就是白色
drawArc(
brush = SolidColor(Color.Black.copy(alpha = 0.2f)),
startAngle = 90F,
//满圈
sweepAngle = 360F,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
drawArc(
brush = SolidColor(orange),
//从12点钟方向开始
startAngle = 270F,
//进度 360F -> 0F
sweepAngle = progress,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
}
试着引入animateFloatAsState()平滑动画效果,为跟上面有区别,我们放大progress 变化率为1秒10F,并且引入布局方便同时查看2种效果:
var progress by remember { mutableStateOf(360f) }
val animateCurrentProgress by animateFloatAsState(
targetValue = progress,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing)
)
LaunchedEffect(key1 = progress) {
if (progress <= 0F) { progress = 360F }
delay(1000)
progress -= 10F
}
Row {
Canvas(
modifier = Modifier
.padding(40.dp)
.size(180.dp)
) {
drawArc(
brush = SolidColor(Color.Black.copy(alpha = 0.2f)),
startAngle = 90F,
sweepAngle = 360F,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
drawArc(
brush = SolidColor(orange),
startAngle = 270F,
sweepAngle = progress,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
}
Canvas(
modifier = Modifier
.padding(40.dp)
.size(180.dp)
) {
drawArc(
brush = SolidColor(Color.Black.copy(alpha = 0.2f)),
startAngle = 90F,
sweepAngle = 360F,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
drawArc(
brush = SolidColor(orange),
startAngle = 270F,
//观察animateCurrentProgress 更平滑
sweepAngle = animateCurrentProgress,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
}
}
效果如下:
简单放入一个Text就是一个倒计时(计时器用delay是不科学的,这里只做示例):
var progress by remember { mutableStateOf(360f) }
var timer by remember { mutableStateOf(36) }
val animateCurrentProgress by animateFloatAsState(
targetValue = progress,
animationSpec = tween(durationMillis = 1000, easing = LinearEasing)
)
LaunchedEffect(key1 = progress) {
if (progress <= 0F) {
progress = 360F
timer = 36
}
delay(1000)
progress -= 10F
timer -= 1
}
Box(
modifier = Modifier
.padding(40.dp)
.size(180.dp)
) {
Canvas(Modifier.fillMaxSize()) {
drawArc(
brush = SolidColor(Color.Black.copy(alpha = 0.2f)),
startAngle = 90F,
sweepAngle = 360F,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
drawArc(
brush = SolidColor(orange),
startAngle = 270F,
sweepAngle = animateCurrentProgress,
useCenter = false,
style = Stroke(width = 20.dp.value, cap = StrokeCap.Round)
)
}
Box(modifier = Modifier.align(Alignment.Center)) {
Text(text = timer.toString())
}
}
效果图:
再回到我们的自定义背景项目,我们引入动画去改变背景色:
val infiniteTransition = rememberInfiniteTransition()
val animationSpec: InfiniteRepeatableSpec<Color> = infiniteRepeatable(
animation = tween(3000, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
)
val colorFirst by infiniteTransition.animateColor(
initialValue = orange,
targetValue = lightPink,
animationSpec = animationSpec
)
val colorSecond by infiniteTransition.animateColor(
initialValue = lightPink,
targetValue = lightPurple,
animationSpec = animationSpec
)
val colorThird by infiniteTransition.animateColor(
initialValue = lightPurple,
targetValue = orange,
animationSpec = animationSpec
)
Spacer(
Modifier
.fillMaxSize()
.drawBehind {
val brushBackground = Brush.verticalGradient(
listOf(colorFirst, colorSecond, colorThird),
0f,
size.height
.toDp()
.toPx(),
TileMode.Mirror
)
drawRect(brush = brushBackground)
})
效果图:
再做一些有趣的事情,比如再背景上加入冒泡效果:
这个效果可以继续优化,比如加入碰撞检测合并气泡,加入根据大小线性拟合出速度!