前言
相较于传统的XML布局构建方式,Compose旨在使用更少的代码实现一种新型的布局方式,为了迎合Google官方的首选开发语言Kotlin,Compose只能使用Kotlin语言开发,从《Compose编程思想》这个专题开始,借助Google官方开发文档,带领大家从Compose入门到高手的进阶。
1 Compose入门
1.1 声明式UI
如果对flutter有了解,或者有开发经验的伙伴,对于声明式UI可能就非常熟悉,它的核心思想在于使用代码(Kotlin或者Dart)实现布局细节,而且不需要手动更新UI,什么意思呢?
先从传统的Java实现方式,布局文件是在xml布局文件中声明,如下:
xml
<TextView
android:id="@+id/tv_ui"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UI绘制"/>
通过android:text
属性可以预先设置文本展示,也可以通过findViewById的方式获取TextView实例对象,通过动态设置文案的方式刷新页面。
java
val view = findViewById<TextView>(R.id.tv_ui)
view.text = "动态修改";
而声明式UI(以Compose为例)则是通过可组合函数构建UI,如下:
kotlin
@Composable
fun showText() {
val name by remember { mutableStateOf("Alex") }
Text(text = name)
}
例如我们要展示文本文案,那么可以使用Text
组件,类似于Java中的TextView
,例如name
设置为Text
需要展示的文案,当name
的值发生变化的时候,Text
展示的文案也会发生变化,但是这个变化是因为值发生变化从而导致组件自动刷新,而不是Java中手动设置值(即主动调用setText方法)刷新。
1.2 Compose组件思想
一个app基本的元素,就是文本和图片,在Compose中对应的组件为Text
和Image
,为什么不用TextView
或者ImageView
这种我们常见的Android平台中的控件命名方式?
其实这里就涉及到了Compose组件的思想 - 独立于Android平台 。独立于Android平台并不意味着脱离了Android平台,像Text
最终底层还是调用了Android原生的drawText
和drawTextRun
函数,但是并没有使用任何Android中的组件,这样做的好处就是能够实现多平台(multi-platform)。
多平台,意味着可以在桌面版(Windows、linux)、WEB、IOS中使用同一套代码,只需要区分平台即可。
因此Compose开始对所有的组件起新的名字,Text
对应TextView
,Image
对应ImageView
,RecyclerView
对应LazyColumn
等等,与传统的Android组件并没有任何的关系,但是底层依然采用了Android原生的API,为什么Compose还要使用Android原生的API,是因为Compose需要跟原生的View交互,因为它绕不开原生。
如果不想跟原生的View有一丝的牵连,那么就是Flutter了,它是直接在NDK的层面与Skia渲染器打交道了,既然深入了底层,自然跟Android原生组件没有任何关系了。
所以总结一下,Compose组件的思想就是绕开了原生的Android组件,直接调用Android底层API渲染,并没有完全脱离原生。
1.3 Compose中那些原生布局的平替
在Android的原生布局中,常用的有约束布局(ConstrainLayout)、线性布局(LinearLayout)、帧布局(FrameLayout)、ScrollView、RecyclerView、ViewPager。
那么在Compose中对应的组件是什么呢?
- 帧布局 - Box
kotlin
@Composable
fun showLayout() {
Box {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.size(100.dp)
)
Text(text = "Alex")
}
}
- 线性布局 - Column / Row
kotlin
@Composable
fun showLayout() {
Column {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.size(100.dp)
)
Text(text = "Alex")
}
}
Column
是vertical
属性;Row
是horizontal
属性。
- 相对布局 - Box
这里需要提一点的就是,相对布局在传统UI中用于定位子View的相对位置,以及View之间的位置关系,在Compose中依然可以使用Box作为平替,而位置关系则是使用Modifier
来完成对应关系。
kotlin
@Composable
fun textRelativeLayout() {
Box {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.size(100.dp)
)
Text(
text = "Alex",
modifier = Modifier.align(Alignment.Center)
)
}
}
例如,使用Alignment.Center
值就可以让Text
显示在父容器的中心位置。
- ConstraintLayout
在Compose中,拥有与ConstraintLayout同名的组件,用于处理比RelativeLayout更多的功能,这里不做赘述,会有专题介绍如何在Compose中使用ConstraintLayout。
- RecyclerView - LazyColumn
kotlin
@Composable
fun testRecyclerView() {
val datas = mutableListOf("A", "B", "C", "D")
LazyColumn {
items(datas) { item ->
Box(modifier = Modifier.size(50.dp)) {
Text(text = item,
modifier = Modifier.align(Alignment.Center))
}
}
}
}
LazyColumn需要一个动态的列表数据,添加到items
中,就可以展示全部的列表数据,不需要Adapter
、ViewHolder
、LayoutManager
。
而且当我们需要给列表加上head或者foot的时候,如果使用RecyclerView,那么就需要在数据项的第0个位置和最后一个位置加上view,那么使用LazyColumn则不需要,直接通过item
就可以添加一个ItemView。
kotlin
@Composable
fun testRecyclerView() {
val datas = mutableListOf("A", "B", "C", "D")
LazyColumn {
// 添加一个头部
item {
Text(text = "这是列表的头部")
}
items(datas) { item ->
Box(modifier = Modifier.size(50.dp)) {
Text(text = item,
modifier = Modifier.align(Alignment.Center))
}
}
}
}
当然除了LazyColumn,还有LazyRow
,属于横向的滑动。除此之外,LazyColumn/LazyRow也有类似于RecyclerView的缓存机制,保证列表的顺畅滑动。
除此之外,像ScrollerView、ViewPager,在Compose中平替的组件还在孵化中,像ViewPager对应的Pager(VerticalPager/HorizontalPager)组件,目前还在测试阶段,如果在项目中想要实现ViewPager的功能,可以自行实现,或者使用原生的ViewPager。
2 Modifier详解
Modifier在Compose中是一个非常重要的组成,像我们在传统的UI中,需要声明每个组件的间隔或者相对于父容器的间隔,通常使用margin或者padding来实现。
xml
<TextView
android:id="@+id/tv_ui"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="UI绘制"
android:layout_marginBottom="20dp"
android:paddingTop="20dp"/>
而在Compose当中,会使用Modifier来完成,在Compose当中的每个组件中,都有一个Modifier参数变量。
kotlin
@Composable
fun Text(
text: String,
// 可以设置Modifier
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
// ......
) {
2.1 设置组件padding 和 margin
如下一个可组合函数,垂直线性布局中,有一个Image和Text,其中通过Mofifier
设置了Image的padding为12dp,整个线性布局的背景是蓝色的,也是通过Modifier的background
属性设置的。
kotlin
@Composable
fun testModifier() {
Column(modifier = Modifier.background(Color.Blue)){
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
Text(text = "测试Modifier")
}
}
如果设置margin属性,我们发现Modifier没有margin
这个函数,那么在Compose中如何设置margin呢?可以通过Spacer
组件来设置。
kotlin
@Composable
fun testModifier() {
Column(modifier = Modifier.background(Color.Blue)) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
Spacer(modifier = Modifier.size(10.dp))
Text(
text = "测试Modifier",
modifier = Modifier.background(Color.Red)
)
}
}
看下效果:
2.2 match_parent和wrap_content如何实现
其实Modifier除了设置padding
和background
之外,还可以设置组件的大小,当然与传统的XML布局必须要设置layout_width和layout_height不同的是,Compose默认宽高均为wrap_content,不需要强制设置。
如果想要组件实现match_parent,那么可以使用Modifier的fillMaxHeight
、fillMaxWidth
、fillMaxSize
函数实现宽高的配置。
kotlin
@Composable
fun testModifier() {
Column(
modifier = Modifier
.background(Color.Blue)
.fillMaxHeight()
) {
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
Spacer(modifier = Modifier.size(10.dp))
Text(
text = "测试Modifier",
modifier = Modifier.background(Color.Red)
)
}
}
如果要设置一个固定的值,那么可以使用Modifier的width
、height
、size
函数,那么就可以配置组件的具体宽高,如果组件是一个正方形,可以直接调用size
函数。
kotlin
Image(
painter = painterResource(id = R.drawable.ic_launcher_background),
contentDescription = null,
modifier = Modifier
.padding(12.dp)
.width(100.dp)
.height(80.dp)
)
2.3 什么情况下会使用Modifier
假如,我们要设置Text的文字大小,和文字颜色,我们可以使用Modifier来设置吗?其实不能,这两个属性其实需要通过Text构造方法中的参数设置。
kotlin
Text(
text = "测试Modifier",
modifier = Modifier.background(Color.Red),
fontSize = 20.sp,
fontWeight = FontWeight(400),
color = Color.White
)
像文字的颜色、字重、字号都只能通过Text的构造函数中的参数设置,而不能使用Modifier。那么什么情况下会使用Modifier呢?
其实Modifier属于公共属性的集合。其实不难理解,对于Text来说它是展示文本的组件,字号、字重属于其独有的属性,而如果在Modifier中维护这些属性,对于布局、Image这些来说根本用不到的属性,反而会导致Modifier变得越来越臃肿。
所以之后在自定义Compose组件的时候,对于组件的特有属性,需要放在构造参数中。
2.4 点击事件
在Android传统UI中,每一个View都可以被设置点击事件监听器,无论View还是ViewGroup都是可被点击的,因此Compose中点击事件也是放在了Modifier中,因为属于通用的能力。
kotlin
Text(
text = "测试Modifier",
fontSize = 20.sp,
modifier = Modifier.background(Color.Red)
.padding(12.dp)
.clickable {
},
fontWeight = FontWeight(400),
color = Color.White
)
在Modifier中提供了clickable
函数,这里需要注意一点,clickable
的摆放顺序是有讲究的,因为我们给按钮加了一个padding,所以clickable
放在了padding后面,那么点击时热区不包括padding,如下所示:
如果想要热区包括padding,那么在声明clickable
时,需要放在padding的前面,可以这么理解,想要响应点击事件的区域,需要放在clickable之后。
kotlin
Text(
text = "测试Modifier",
fontSize = 20.sp,
modifier = Modifier.background(Color.Red)
.clickable {
}
.padding(12.dp),
fontWeight = FontWeight(400),
color = Color.White
)
经过前面的这些介绍,相信伙伴们对于Compose有了一些基础的了解,相信掌握了前面的这些知识,通过Compose能够写出一些页面了吧,而且相较于传统的UI,这种声明式的框架显得更加灵活且高效,当然这也是Compose的冰山一角,后续我将会介绍更加高阶的Compose知识。