简介
Jetpack Compose是用于构建原生Android界面的新工具包。它是一种声明式的UI布局,其官方声称可简化并加快Android上的界面开发,使用更少的代码、强大的工具和直观的Kotlin API,快速让应用生动而精彩。
官网:developer.android.com/jetpack/com…
我这里也写了一个Compose的Demo,可以对照着看:github.com/dreamgyf/Co…
这个Demo实现了:
Compose替代传统布局
- 网格列表效果,类似于传统布局中的
RecyclerView配合GridLayoutManager
-
在传统View中使用Compose
-
在Compose中使用传统View
-
自定义布局
前置工作
使用Jetpack Compose需要先引入一些依赖:
dependencies {
implementation 'androidx.core:core-ktx:1.7.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
//网络图片加载三方库
implementation "io.coil-kt:coil-compose:1.4.0"
}
可组合函数
Jetpack Compose是围绕着可组合函数构建起来的,这些函数以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程。此类函数有几个要点:
- 所有可组合函数必须使用
@Composable注解修饰 - 可组合函数可以像正常函数一样接受参数
@Composable
fun Demo(name: String) {
Text(text = "Hello, ${name}!")
}
- 可组合函数内部可以书写正常代码(譬如可以通过
if else控制显示的控件)
@Composable
fun Demo(showPic: Boolean) {
if (showPic) {
Image(
painter = painterResource(id = R.drawable.demo),
contentDescription = null
)
}
Text(text = "Hello, compose!")
}
单位
Android常用的单位dp,sp等,在Compose中以类的形式被定义,使用的方式也很简单,Compose借助了kotlin的扩展属性,扩展了Int,Double,Float三个基础类,使用方式如下:
//dp
1.dp; 2.3f.dp; 4.5.dp
//sp
1.sp; 2.3f.sp; 4.5.sp
资源
如何在Compose中使用资源呢,可以通过xxxResource方法
//图片资源
fun painterResource(@DrawableRes id: Int): Painter
//尺寸资源
fun dimensionResource(@DimenRes id: Int): Dp
//颜色资源
fun colorResource(@ColorRes id: Int): Color
//字符串资源
fun stringResource(@StringRes id: Int): String
//字体资源
fun fontResource(fontFamily: FontFamily): Typeface
Modifier
Modifier是Compose中的布局修饰符,它控制了布局的大小,padding,对齐,背景,边框,裁切,点击等属性,几乎所有的Compose布局都需求这项参数,是Compose布局中的重中之重
这里介绍一些常用的基本属性,文中没列到的属性可以去官网查看:developer.android.com/jetpack/com…
尺寸
fillMaxWidth和fillMaxHeight相当于xml布局中的match_parentfillMaxSize相当于同时设置了fillMaxWidth和fillMaxHeightwrapContentWidth和wrapContentHeight相当于xml布局中的wrapContentwrapContentSize相当于同时设置了wrapContentWidth和wrapContentHeightwidth和height则是设置固定宽高,单位为Dpsize相当于同时设置了width和heightweight属性仅在Row或Column的内部作用域中可以使用,相当于传统LinearLayout布局中的weight属性
padding
padding方法有几个重载,这些API很简单,看参数就很容易能明白意思
对齐
align属性,使控件可以在父布局中以一种方式对齐,相当于xml布局中的layout_gravity属性。另外还有alignBy以及alignByBaseline属性可以自行研究
绘图
background设置背景,不过不能设置图片,如果想以图片作为背景可以使用Box布局,在底部垫一个Image控件alpha设置透明度clip裁剪内容,这个功能很强大,可以直接将视图裁出圆角,圆形等形状
操作
clickable方法,可以设置控件的点击事件回调combinedClickable方法,可以设置控件的点击、双击、长按事件回调selectable方法,将控件配置为可点击,同时可以设置点击事件
滚动
horizontalScroll:使控件支持水平滚动verticalScroll:使控件支持垂直滚动
注意事项
在Modifier中设置属性的前后顺序是很重要的,譬如想要一个背景为蓝色的圆角布局,需要先设置clip,再设置background,反过来background会超出圆角范围
Spacer
Compose中没有了margin的概念,可以用Spacer替代,Spacer为留白的意思,使用起来也很简单
//水平间隔8dp
Spacer(modifier = Modifier.width(8.dp))
基础布局
Row & Column
这是两个基本布局组件,其中Row为水平布局,Column为垂直布局,他们俩接受的参数相似,其中两个参数为horizontalArrangement和verticalAlignment,他们一个表示水平布局方式,一个表示垂直布局方式,他们默认值为START和TOP,这两个参数用起来就和传统布局的gravity参数一样
Box
Box也是一种基本布局组件,Box布局中的组件是可以叠加的,类似传统布局中的FrameLayout,可以通过contentAlignment参数调整叠加的方式,其默认值为TopStart,叠加到左上角,这个参数也和FrameLayout的gravity参数一样
基础控件
Text
文本控件,对应传统控件TextView,它有以下一些属性
| 属性 | 说明 |
|---|---|
| text | 文本内容 |
| color | 文字颜色 |
| fontSize | 文字大小 |
| fontStyle | 文本样式(可以设置斜体) |
| fontWeight | 字重(粗体等) |
| fontFamily | 字体 |
| letterSpacing | 文字间距 |
| textAlign | 文本对齐方式 |
| lineHeight | 行高 |
| maxLines | 最大行数 |
| ... | ... |
Image
图片控件,对应传统控件ImageView,它有以下一些属性
| 属性 | 说明 |
|---|---|
| painter | 图片内容 |
| contentDescription | 无障碍描述(可为null) |
| alignment | 对齐方式 |
| contentScale | 缩放方式(和scaleType属性类似) |
| alpha | 透明度 |
| ... | ... |
在开发中经常会面对从网络价值图片的情况,这时候可以借助一些第三方库来解决,这里以coil库为例:
- 先添加依赖
implementation "io.coil-kt:coil-compose:1.4.0"
- 使用
Image(
modifier = Modifier
.size(68.dp, 68.dp)
.clip(RoundedCornerShape(6.dp)),
contentScale = ContentScale.Crop,
painter = rememberImagePainter(picUrl), //使用rememberImagePainter方法填入图片url
contentDescription = null
)
列表
Compose有两种组件LazyRow和LazyColumn,一种水平,一种垂直,对应着传统UI中的RecyclerView,用这些组件可以方便的构建列表视图,它们需要提供一个LazyListScope.()块描述列表项内容
LazyListScope的DSL提供了多种函数来描述列表项:
//用于添加单个列表项
fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit)
//用于添加多个列表项
fun items(
count: Int,
key: ((index: Int) -> Any)? = null,
itemContent: @Composable LazyItemScope.(index: Int) -> Unit
)
//用于添加多个列表项
fun <T> LazyListScope.items(
items: List<T>,
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit
)
示例:
val list = mutableListOf(0, 1, 2, 3, 4)
LazyColumn {
//增加单个列表项
item {
Text(text = "First item")
}
//增加5个列表项
items(5) { index ->
Text(text = "Item: $index")
}
//增加5个列表项
items(list) { listItem ->
Text(text = "Item: $listItem")
}
//增加单个列表项
item {
Text(text = "Last item")
}
}
可以使用contentPadding为内容添加内边距,使用verticalArrangement或horizontalArrangement,以Arrangement.spacedBy()为列表项之间添加间距
状态
在Compose中,数据的更新和传统命令式UI不同,是通过一种可观察类型对象,当一个可观察类型对象发生改变时,这个对象对应观察的部分会发生重组,从而自动更新UI
可观察类型MutableState<T>通常是通过mutableStateOf()函数创建的,这个对象的value发生变化时,对应UI也会跟着随之变化
//这里使用了kotlin的by关键字,是一种代理模式
//如果使用 = 的话,这个对象的类型会发生变化,需要count.value这样使用它的值
//var count = mutableStateOf(0)
var count by mutableStateOf(0)
@Composable
fun Demo(count: Int) {
Column {
Text(text = "count: ${count}")
Button(onClick = { addCount() }) {
Text(text = "add count")
}
}
}
fun addCount() {
//++count.value
++count
}
@Preview
@Composable
fun Preview() {
//当点击Button时,触发点击事件,更新可观察对象count,触发UI重组
//Demo(count.value)
Demo(count)
}
关于Context
在Compose中可以通过LocalContext.current获得当前Context
在传统View中使用Compose
可以在一个传统布局xml中插入一个ComposeView
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/hello_world"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello from XML layout" />
<!-- 插入ComposeView -->
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
然后在代码中设置这个ComposeView
findViewById<ComposeView>(R.id.compose_view).setContent {
Text("Hello Compose!")
}
在Compose中使用传统View
可以使用AndroidView这个composable函数,这个函数接受一个factory参数,这个参数接受一个Context,用于构建传统View,要求返回一个继承自View的对象
@Composable
fun Demo() {
Column {
Text(text = "Compose Text")
AndroidView(factory = { context ->
//这里也可以使用LayoutInflater从xml中解析出一个View
TextView(context).apply {
text = "传统TextView"
}
})
}
}
自定义UI
在Compose中,如果想要自定义一些简单的UI是很简单的,只需要写一个Composable函数就可以了,我们主要学习一下怎么自定义一些复杂的UI
我们先看一下怎么自定义一个布局,对应着传统UI中的ViewGroup,以一个简单的例子来说,我们自定义一个布局,让其中的子布局呈左上到右下依次排列:
@Composable
fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(modifier = modifier, content = content) { measurables, constraints ->
//测量每个子布局
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
//设置布局大小为最大可容纳大小
layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = 0
var yPosition = 0
//放置每个子View
placeables.forEach { placeable ->
placeable.placeRelative(x = xPosition, y = yPosition)
//下一个子View的坐标为上一个子View的右下角
xPosition += placeable.width
yPosition += placeable.height
}
}
}
}
我们再看一个使用Canvas自定义View的方式,这个更简单,就是画一条水平线:
@SuppressLint("ModifierParameter")
@Composable
fun HorizontalLine(modifier: Modifier = Modifier.fillMaxWidth()) {
Canvas(modifier = Modifier
.then(modifier), onDraw = {
drawLine(color = Color.Black, Offset(0f, 0f), Offset(size.width, 0f), 2f)
})
}
我们将两者一起用一下看看效果
@Preview(showBackground = true)
@Composable
fun Preview() {
MyLayout {
Text(text = "Text1")
HorizontalLine(Modifier.width(50.dp))
Text(text = "Text2")
}
}
其实Compose中的自定义UI的思路和传统自定义View是一样的,只不过需要熟悉Compose中的各种Api才能灵活运用它