你是否受够了 Android 中 UI 编写的体验——在 xml 文件中编写复杂的层级结构和繁多的属性,动态化的视图逻辑又被分裂到 Activity
中?哦,这该死的友好度和割裂感!
这两年,Flutter 大行其道,不论是网上的讨论度还是实际的落地项目,风头一时无两。所以从这个角度来说,作为 UI 框架的 Flutter,无疑是成功的。本着借鉴的思想(或许吧,谁知道呢),Android 在 Jetpack 项目中新增了一套全新的视图开发套件——Compose。它有着和 Flutter 一样好看的(姑且这么认为吧)外表,但究竟只是一个好看的花瓶还是才貌双全,这得我们自己去寻找答案。
Compose 当前还处于测试版本,想要使用它,我们需要首先下载 Android studio 的 canary 版本以提供支持。你可以在这里下载或者在你现有的 Android studio 中打开 File -> Settings -> Appearance & Behavior -> System Settings -> Updates 菜单,然后切换到 canary 渠道再点击 Check Now 按钮即可更新到最新的 canary 版本。
一、布局
1.可组合函数
使用 Compose 创建一个界面是简单的,只需通过 @Composable
注解定义一个可组合函数,在函数中返回界面元素组件即可。
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
复制代码
该函数可以接收参数。函数内可以是一个组件,也可以是多个组件的组合。
通过 setContent
方法块可以设置页面内容,类似于之前的 setContentView()
方法。
setContent {
Text(text = "Hello Compose!")
}
复制代码
相比于之前的界面书写过程,Compose 更「神奇」的一个体现在于它可以直接在 Android studio 中预览我们编写的界面和组件,而无需让程序运行在设备中。
我们只需要在可组合函数的基础上再新增一个 @Preview
注解,但是需要注意的是,预览函数不接受参数,所以比较好的做法是在可组合函数的基础上编写其对应的预览函数。
@Preview
@Composable
fun DefaultPreview() {
Greeting("Android")
}
复制代码
预览函数对你的应用在设备上的最终呈现不会产生影响,Android studio 提供了一个预览窗口可以实时看到预览函数所呈现的效果。
2.布局
我们编写的应用界面几乎任何时候都不会是简简单单的单一的控件,而是一定数量的独立控件在空间上的一种组合。
首先,我们就盲猜,如果我想竖直方向排列三个文字组件,肯定不是像下面这样随便组合三个 Text
控件。它怎么可能那么聪明,能知道你是想横着排还是竖着排,想并排排还是旋转开。怎么可能有人比苏菲更懂你!
@Composable
fun VerticalText() {
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
复制代码
那,就组合喽。
@Composable
fun VerticalText() {
Column {
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码
给三个 Text
约定个竖框框,它们就能乖乖地排起队。
这里,悄摸摸地说一句,这要是没有偷瞄 Flutter 的考卷 向优秀的思想借鉴,我把三个 Text
布局在我脑门上!
当然,只有这么生硬的排列可不行,我们还需要加点属性,使得整个布局更和谐点——例如,加点边距。
我们希望给 Column
加一个内边距,那么我们就应该给 Column
添加一个属性。Modifier
类用来给组件添加装饰或者行为,如背景、边距、点击事件等。
@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码
现在,为了让界面看起来不那么单调,我们给这个界面加上下面这一张图片。

将这张图片拷贝到 drawable
资源文件夹下面,然后通过下面的方式引用。
@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null
)
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码
Image
的其中一个构造函数支持以下参数,其中 painter
参数和 contentDescription
参数没有默认值,为必传参数。
这样,图片就被构造出来啦,看一下效果:
那怎么该对图片进行一些约束呢?作为一个头图,我不希望它这么哗众取宠,做图片要低调一点。
在上面,我们认识了 Modifier
,那就寻求它的帮助,让我们的图片小一些吧。
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp),
contentScale = ContentScale.Inside
)
复制代码
借助 Modifier
将图片的高度和宽度分别进行限定。然后通过 contentScale
参数对图片的缩放方式进行约束。ContentScale.Inside
保持图片比例不变的情况下尽可能地充满父控件的体积。
把上面的 Image
放入 preview 方法,看一下效果:
现在头图就被我们拿捏得死死的,但是它还不是很好看,没脖子,加个脖子。
@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp),
contentScale = ContentScale.Inside
)
Spacer(modifier = Modifier.height(16.dp))
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码
这样是不是好看多了,嗯,是的。
3.Material Design
谷歌霸霸的产品当然是支持 Material Design 的,那咱就看看。
做头图不要锋芒毕露,做图处事要圆滑一点。给头图加个圆角是个不错的想法。
在 Android 传统的 UI 编写中,圆角图片一直没有很简单的解决方案,需要通过诸如自定义 ImageView
的方式来实现。但是,朋友们,当你使用 Compose 框架的时候,只需要一行代码就可以圆角图片的显示!家祭无忘告乃翁。
@Preview(showBackground = true)
@Composable
fun VerticalText() {
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp)
.clip(shape = RoundedCornerShape(4.dp)),
contentScale = ContentScale.Inside
)
Spacer(modifier = Modifier.height(16.dp))
Text("Hello World!")
Text("Hello Again World!")
Text("How old are you, World!")
}
}
复制代码
这里还是通过 Modifier
来实现需求,怎么样,现在的头图是不是圆滑可爱了很多。
头图这么求上进,文字也不能落后,一篇好的文章要主次分明,错落有致。
声明 Typography
对象,然后给 Text
添加 style
属性,来控制文字的样式。
@Preview(showBackground = true)
@Composable
fun VerticalText() {
val typography = MaterialTheme.typography
Column(
modifier = Modifier.padding(16.dp)
) {
Image(
painter = painterResource(id = R.drawable.hello_world_new_black),
contentDescription = null,
modifier = Modifier
.width(126.dp)
.height(62.dp)
.clip(shape = RoundedCornerShape(4.dp)),
contentScale = ContentScale.Inside
)
Spacer(modifier = Modifier.height(16.dp))
Text("Hello World!", style = typography.h3)
Text("Hello Again World!", style = typography.body1)
Text("How old are you, World!", style = typography.body2)
}
}
复制代码
Typography
提供如下预设属性,囊括标题、子标题、段落体、按钮等。
最终效果如下:
怎么样,是不是主次开始变得分明了?结构变得清晰了?情节展开得顺滑了?故事开始自然了?……
当然,其他的诸如最大行数、字体、对齐方式等都可以被配置。
二、主题
基本布局已经差不多啦,那么我们再来搞一些共性的东西,就像我们黄种人都有一样的肤色——散在土地里的黄,有种顽强,非常东方……
以前的 View 系统其实也有关于 theme 的定义,那些被定义的 style,在官方定义的一系列 theme 的基础上加以扩展,形成我们 app 的主题。
Compose 框架提供了 Material Design 的实现,Material Design Theme 自然也被应用到 Compose 中,Material Design Theme 包括了对颜色、文本样式和形状等属性的定义,咱们自定义这些属性后,包括 button、cards、switches 等控件都会相应的改变它们的默认样式。
1.颜色
颜色在前端开发中真的是无处不在了,Color
可以帮助我们快速地构建颜色模型。
你可以泡着吃:
val red = Color(0xffff0000)
复制代码
可以扭着吃:
val blue = Color(red = 0f, green = 0f, blue = 1f)
复制代码
欸,你还可以干吃:
val black = Color.Black
复制代码
只要你喜欢,你甚至可以空翻360度加转体一周半的时候吃:
// 我不会空翻,也不会转体,期待你的表现,加油!
复制代码
Compose 提供了 Colors
数来创建成套的浅色或深色:
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200,
onPrimary = Color.Green
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant Customize= Purple700,
secondary = Teal200,
onPrimary = Color.Green
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
复制代码
然后,就可以传递给 MaterialTheme
使用喽:
@Composable
fun TestComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
复制代码
怎么样,还自动适配深色模式。
而且,我们也可以随时随地获取到主题色:
Text(
text = "Hello theming",
color = MaterialTheme.colors.primary
)
复制代码
表面颜色和内容颜色又是另一个概念了,许多组件都接受一对颜色和「内容颜色」:
Surface(
color: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(color),
…
TopAppBar(
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
…
复制代码
这样一来,您不仅可以设置可组合项的颜色,而且还能为包含在可组合项中的内容提供默认颜色。默认情况下,许多可组合项都使用这种内容颜色。例如,Text
的颜色基于其父项的内容颜色,而 Icon
:「俺也一样」,它可以使用该颜色来设置其色调。
contentColorFor()
方法可以为任何主题颜色检索适当的“on”颜色。例如,如果您设置 primary
背景,就会将 onPrimary
设置内容颜色。如果您设置非主题背景颜色,还应指定合理的内容颜色。使用 LocalContentColor
可检索与当前背景形成对比的当前内容颜色。
我们以上面自定义的 Theme 来试验,使用它作为我们的主题:
@Preview
@Composable
fun TestColor() {
TestComposeTheme {
Button(onClick = {}) {
Text(
"hello world"
)
}
}
}Customize
复制代码
效果:
2.字体排版
字体排版主要通过 Typography
和 TextStyle
类来完成。Typography
构造函数可以提供每种样式的默认值,因此您可以省略不希望自定义的任何样式:
val Rubik = FontFamily(
Font(R.font.rubik_regular),
Font(R.font.rubik_medium, FontWeight.W500),
Font(R.font.rubik_bold, FontWeight.Bold)
)
val MyTypography = Typography(
h1 = TextStyle(
fontFamily = Rubik,
fontWeight = FontWeight.W300,
fontSize = 96.sp
),
body1 = TextStyle(
fontFamily = Rubik,
fontWeight = FontWeight.W600,
fontSize = 16.sp
)
/*...*/
)
MaterialTheme(typography = MyTypography, /*...*/)
复制代码
如果您希望自始至终使用同一字体,请指定 defaultFontFamily
参数,并省略所有 TextStyle
元素的 fontFamily
:
val typography = Typography(defaultFontFamily = Rubik)
MaterialTheme(typography = typography, /*...*/)
复制代码
使用时,可以从主题检索 TextStyle
,如以下示例所示:
Text(
text = "Subtitle2 styled",
style = MaterialTheme.typography.subtitle2
)
复制代码
3.形状
Compose 中可以轻松地定义各种形状,比如圆角或者操场跑道形状,在传统 View 系统中实现都比较麻烦。
我们现在修改一下上面的 Button 的形状来看看效果:
val Shapes = Shapes(
small = CutCornerShape(
topStart = 16.dp,
topEnd = 0.dp,
bottomStart = 16.dp,
bottomEnd = 0.dp
),
medium = RoundedCornerShape(percent = 50),
large = RoundedCornerShape(0.dp)
)
复制代码
这里有一点需要注意的是,默认情况下,许多组件使用这些形状。例如,Button、TextField 和 FloatingActionButton 默认为 small,AlertDialog 默认为 medium,而 ModalDrawerLayout 默认为 large。如需查看完整的对应关系,请参阅形状方案参考文档。
三、列表
列表也是个常见的家伙,Android View 系统中早期的 ListView
和后来的 RecyclerView
, Flutter 里的 ListView
等。
一个列表就是许多个元素排排站,整齐笔直。那一个纵向(或横向)的布局中动态地添加进许多的元素不就好了。
@Composable
fun MessageList(messages: List<Message>) {
Column {
messages.forEach { message ->
MessageRow(message)
}
}
}
复制代码
来,你猜,RecyclerView
是不是这么写的。这里有个最大的问题,假如你是个交际花,好友从这里排到法国,列表多到滑一晚上滑不到头,那么一次加载是不是要耗费巨大的资源,搞不好卡死了王思聪联系不上你那就太不给面了,不好。
RecyclerView
最大的一个优点是它可以懒加载列表项,一次只加载一个屏幕的条目(四舍五入就是我对)。Compose 中可没有 RecyclerView
,但是同样有针对这一问题优化的组件,LazyColumn
和 LazyRow
是垂直和水平方向的懒加载列表控件。我们先来看一下效果:
@Preview
@Composable
fun TestList() {
LazyColumn(modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)) {
// Add a single item
item {
Text(text = "First item")
}
// Add 5 items
items(10000) { index ->
Text(text = "Item: $index")
}
// Add another single item
item {
Text(text = "Last item")
}
}
}
复制代码
这里加载了一万个元素的列表,看看这丝滑的效果吧(建议就着德芙食用)。
我们还可以像上面一样,通过 contentPadding
设置内容边距,verticalArrangement
则是可以设置 item 间间距,以及均匀地排列元素以充满父空间。
比较遗憾地是 LazyColumn
和 LazyRow
暂时无法设置例如添加元素时地动画,期待后续的加入吧。
LazyColumn
可以轻松地实现粘性标题,只需使用 stickyHeader()
函数即可:
// TODO: This ideally would be done in the ViewModel
val grouped = contacts.groupBy { it.firstName[0] }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ContactsList(grouped: Map<Char, List<Contact>>) {
LazyColumn {
grouped.forEach { (initial, contactsForInitial) ->
stickyHeader {
CharacterHeader(initial)
}
items(contactsForInitial) { contact ->
ContactListItem(contact)
}
}
}
}
复制代码
上面的代码演示了如何通过 Map
数据结构实现粘性标题的数据展示。
既然有列表,那么肯定会有宫格列表,LazyVerticalGrid
则能够帮助我们实现需求。更多用法查看相关 API(没错,我就是 LazyBoy,但我的尊严决定了我不会滚)。
在实际项目开发中,我们经常会遇到将数据分页展示的情况,以减少数据请求压力。借助 Paging 3.0 库 可以来进行分页,Paging 库是 Jetpack 中重要的一项新特性,可帮助您一次加载和显示多个小的数据块。按需载入部分数据会减少网络带宽和系统资源的使用量。
如需显示分页内容列表,可以使用 collectAsLazyPagingItems()
扩展函数,然后将返回的 LazyPagingItems
传入 LazyColumn
中的 items()
。与视图中的 Paging 支持类似,您可以通过检查 item
是否为 null
,在加载数据时显示占位符:
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
@Composable
fun MessageList(pager: Pager<Int, Message>) {
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
LazyColumn {
items(lazyPagingItems) { message ->
if (message != null) {
MessageRow(message)
} else {
MessagePlaceholder()
}
}
}
}
复制代码