本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、声明式 UI
在提到 Compose 时,我们总是会听到这是一个声明式的 UI 库。所谓声明式 UI,是和以前的命令式 UI 相对的概念。两者的区别是:
- 在 View 体系编写的 UI 中,当页面发生改变时,开发者需要手动调用 view.setXXX 方法去更新 UI。
- 在 Compose 体系编写的 UI 中,Compose 只负责描述页面的当前状态,只要把当前状态的参数告诉 Compose,它就会根据当前状态绘制出当前的页面。Compose 中的控件无法通过 id 获取到实例,无法通过 view.setXXX 方法更新 UI 控件。
声明式 UI 的一个重要理念是:你只负责根据当前状态描述当前的 UI,框架负责在状态改变时更新 UI。
二、自定义 View
在 View 体系中,编写自定义 View 通常有三种方式:继承布局、继承原生控件、继承 View 手动绘制。
在 Compose 中,通常不再需要使用继承的方式,许多控件本身就支持放置其他控件,比如 Button() 函数:
Button(onClick = {}) {
Text(text = "1")
}
Button() 函数的最后一个参数是 content: @Composable RowScope.() -> Unit,表示一个水平的 Layout,可以在其中随意放置其他控件。实现以前继承于 Button 的自定义 View 的效果。
在 Compose 中绘制 View 和以前是很相似的,通过在 Canvas 中调用 drawXXX 方法即可。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ProgressCircle(270f)
}
}
@Composable
fun ProgressCircle(sweepAngle: Float) {
// Circle diameter
val size = 160.dp
Box(modifier = Modifier.padding(16.dp), contentAlignment = Alignment.Center) {
Canvas(
modifier = Modifier.size(size)
) {
// Circle radius
val r = size.toPx() / 2
// The width of Ring
val stokeWidth = 12.dp.toPx()
// Draw dial plate
drawCircle(
color = Color.LightGray,
style = Stroke(
width = stokeWidth,
pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(1.dp.toPx(), 3.dp.toPx())
)
)
)
// Draw ring
drawArc(
brush = Brush.sweepGradient(
0f to Color.Magenta,
0.5f to Color.Blue,
0.75f to Color.Green,
0.75f to Color.Transparent,
1f to Color.Magenta
),
startAngle = -90f,
sweepAngle = sweepAngle,
useCenter = false,
style = Stroke(
width = stokeWidth
),
alpha = 0.5f
)
// Pointer
val angle = (360 - sweepAngle) / 180 * Math.PI
val pointerTailLength = 8.dp.toPx()
drawLine(
color = Color.Red,
start = Offset(r + pointerTailLength * sin(angle).toFloat(), r + pointerTailLength * cos(angle).toFloat()),
end = Offset((r - r * sin(angle) - sin(angle) * stokeWidth / 2).toFloat(), (r - r * cos(angle) - cos(angle) * stokeWidth / 2).toFloat()),
strokeWidth = 2.dp.toPx()
)
drawCircle(
color = Color.Red,
radius = 5.dp.toPx()
)
drawCircle(
color = Color.White,
radius = 3.dp.toPx()
)
}
}
}
}
在这个例子中,我们绘制了一个表盘。通过 drawCircle 绘制圆形,drawArc 绘制弧线,drawLine 绘制直线。在绘制时,通过 pathEffect 设置绘制效果,通过 brush 参数设置笔刷的样式。
最终效果如下:
三、自定义布局
Compose 中,界面元素都绘制在界面树中,每个界面元素都有一个父元素,还可能有多个子元素。每个元素在父元素中都有一个位置,指定为 (x, y) 位置,还有一个尺寸,指定为 width 和 height。
自定义一个 MyColumn 的代码如下:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyColumn {
Text(text = "Hello")
Text(text = "Hello")
Text(text = "Hello")
}
}
}
@Composable
fun MyColumn(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
var y = 0
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach {
it.placeRelative(0, y)
y += it.height
}
}
}
}
}
在这份代码中,我们通过 Layout() 函数构建一个自定义布局。它需要三个参数:
- modifier 我们已经很熟悉了,表示修饰器,设置 padding、size 之类的
- content 表示子元素
- MeasurePolicy 表示布局方式。
前两个元素由 MyColumn 参数传入,着重看一下第三个参数。
首先通过 measurables 的 measure(constraints) 函数测量出每个元素的位置。然后通过 layout() 函数设置整个布局的宽高,我们将宽高指定为 constraints.maxWidth, constraints.maxHeight,表示此布局的最大宽高。
在 layout 函数中,遍历所有的元素,将其位置调整为 (0, y),其中 y 每次增加此元素的高度。这样就能实现类似 Column 的效果。
运行程序,显示如下:
可以看出,在 Compose 中,自定义 View 和自定义布局的代码量都不多,熟练掌握之后可以提升开发效率。