本文由 Jetpack Compose 团队的 Louis Pullen-Freilich (软件工程师)、Matvei Malkov (软件工程师) 和 Preethi Srinivas (UX 研究员) 共同撰写。
绘制可点击的矩形
fun Button(
text: String,
onClick: (() -> Unit)? = null,
enabled: Boolean = true,
shape: ShapeBorder? = null,
color: Color? = null,
elevation: Dp = 0.dp
) {
// 下面是具体实现
}
public commit
https://github.com/androidx/androidx/commit/d4f91b3a79ced7473e21c7c000edd469d24c318b
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
content: Unit RowScope.() ->
) {
// 下面是具体实现
}
△ 1.0 版本的 Button API
获得开发者反馈
Material Button 类型
https://material.io/components/buttons
为了验证我们的假设和设计方法,我们邀请开发者参与编程活动,并使用 Button API 完成简单的编程练习。编程练习中包括实现下图的界面:
△ 开发者所需开发的 Rally Material Study 的界面
Rally
https://material.io/design/material-studies/rally.html
Button(text = "Refresh"){
}
△ 使用 Button API
// 这里我们有 Padding 可组合函数,但是没有修饰符
Padding(padding = 12.dp) {
Column {
Text(text = "Refresh", style = +themeTextStyle { body1 })
}
}
当时使用样式 API,比如 themeShape 或 themeTextStyle,需要添加 + 操作符前缀。这是因为当时的 Compose Runtime 的特定限制造成的。开发者调查表明: 开发者发现很难理解此操作符的工作原理。从该现象中我们得到的启示是,不受设计者直接控制的 API 样式会影响开发者对 API 的认知。比如,我们了解到某位开发者对这里的操作符的评论是:
就我目前的理解,它是在复用一个已有的样式,或者基于该样式进行扩展。
我感觉只是在这里随意堆叠了一些东西,没有信心能够使其发挥作用。
Button{
text = "Refresh",
textStyle = +themeStyle {caption},
color = rallyGreen,
shape = RoundedRectangleBorder(borderRadius = BorderRadius.circular(5.dp.value))
}
△ 正确自定义 Button 的文字样式、颜色和形状
保持 API 的一致性
在我们的编程活动中,样式给开发人员带来了很多问题。要洞悉其中的原因,我们先回溯一下为什么样式的概念存在于 Android 框架和其他工具包中。
"样式" 本质上是与 UI 相关的属性的集合,可被应用于组件 (如 Button)。样式包含两大主要优点:
1. 将 UI 配置与业务逻辑相剥离
在类似 Compose 的声明式工具包中,会通过设计减少业务逻辑和 UI 的耦合。像 Button 这样的组件,大多是无状态的,它仅仅显示您所传递的数据。当数据更新时,您无需更新它的内部状态。由于组件也都是函数,可以通过向 Button 函数传参实现自定义,如其他函数的操作一样。但是这会增加将 UI 配置从功能配置中剥离的难度。比如,设置 Button 的 enabled = false ,不仅控制 Button 的功能,还会控制 Button 是否显示。
这就引出一个问题: enabled 应该是一个顶层的参数呢,还是应该在样式中作为一个属性进行传递?而对于可用于 Button 的其他样式呢,比如 elevation,或者当 Button 被点按时,它的颜色变化呢?设计可用 API 的一个核心原则是保持一致性。我们发现在不同的 UI 组件中,保证 API 的一致性是非常重要的。
2. 自定义一个组件的多个实例
val LoginButtonStyle = ButtonStyle(
backgroundColor = Color.Blue,
contentColor = Color.White,
elevation = 5.dp,
shape = RectangleShape
)
LoginButtonStyle) { =
"LOGIN") =
}
△ 为登录按钮定义样式
fun LoginButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(onClick = onClick, modifier = modifier, style = LoginButtonStyle) {
Text(text = "LOGIN")
}
}
fun LoginButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier,
shape = RectangleShape,
elevation = ButtonDefaults.elevation(defaultElevation = 5.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Blue, contentColor = Color.White)
) {
Text(text = "LOGIN")
}
}
△ 最终的 LoginButton 实现
@Composable
inline fun OutlinedButton(
modifier: Modifier = Modifier.None,
noinline onClick: (() -> Unit)? = null,
backgroundColor: Color = MaterialTheme.colors().surface,
contentColor: Color = MaterialTheme.colors().primary,
shape: Shape = MaterialTheme.shapes().button,
border: Border? =
MaterialTheme.colors().onSurface.copy(alpha = OutlinedStrokeOpacity)),
elevation: Dp = 0.dp,
paddings: EdgeInsets = ButtonPaddings,
noinline children: @Composable() () -> Unit
Button( =
modifier = modifier,
onClick = onClick,
backgroundColor = backgroundColor,
contentColor = contentColor,
shape = shape,
border = border,
elevation = elevation,
paddings = paddings,
children = children
)
△ 1.0 版本中的 OutlinedButton
去掉样式
https://github.com/androidx/androidx/commit/e386f97dd769da18d9f3103958714e43d797219f
提高 API 的可发现性或可见性
小结
本文以 Jetpack Compose 中 Button API 作为切入点,从原型设计出发,为大家展示了研发团队如何基于开发者反馈、API 一致性和 API 可发现性三个角度,对公共 API 接口进行不断迭代和优化,以及在过程中所遇到的问题。
问题提出和建议反馈
https://issuetracker.google.com/issues/new?component=612128&template=1253476
Google 用户体验调研
推荐阅读
点击屏末 | 阅读原文 | 即刻了解使用 Jetpack Compose 打造更出色的应用