1
头图

公众号名片
作者名片

Compose 简介

Jetpack Compose 是在 2019 Google I/O 大会上发布的新库,直到 2021 年 7 月发布 release 版本 1.0.0。它的特点是可以用更少的 Kotlin
代码,更便捷地在 Android 平台上完成 UI 的开发。

为什么会推出 Compose

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android. Quickly bring your app to life with less code, powerful tools, and intuitive Kotlin APIs.

从官网的描述就可看出使用 Compose 可以简化在 Android 上 UI 的开发,可以显著减少创建页面的时间,更具有‘现代化’。

随着手机硬件的更新迭代,在手机上构建复杂度比较高的页面以满足业务的需求成为了可能,基于传统 XML
构建的方式对应的控件也越来越多,而维护个控件之间的状态的同步显得越来越难以维护,需要花费不小的精力来保持各控件状态的统一上。

基于这一点,Android 推出了 Compose,Compose 声明的 UI 不可变,无法被外界引用,无法持有状态,用 @Composable 声明以一个“纯函数”的方式运行,当 State
变化时函数重新执行刷新 UI,可以更好地贯彻声明式 UI 的特点。

什么是声明式 UI

传统的界面编写都是通过命令式的编程方式来完成的,比如在 Android 上是通过 xml 构建出来的不同类型的 view,然后需要改变状态时直接调用该 view 的方法来发生改变。

 // 通过 findViewById 来查找对应的 TextView
var tv: TextView = findViewById(R.id.tv)
// 直接调用方法来改变 TextView 的颜色
tv.setColor(red)

声明式 UI 则只需要描述当前的 UI 状态,不需要为不同 UI 状态的切换进行单独的控制,当需要改变时只需要改变对应的状态,剩下的工作就交由框架来完成。

      // 当改变 name 状态值,就会自动更新 UI 状态
      Text(
            "hello ${name}",
            modifier = Modifier.background(color = Color.Blue)
        )
     

基本用法

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApplicationTheme {
        Greeting("Android")
    }
}
  • @Composable: 可以看到,只要涉及到 Compose 构建的控件的方法都有 @Composable 的注解,只能在同样被 @Composable 注解的方法中调用。
  • @Preview: 在方法前加上 @Preview 注解就能在不运行程序的情况下看到相关的布局。在 Android Studio 的右上角会有三个选项,如果选择 Split 和 Design
    就可以看到对应的显示效果了。

Android Studio 预览

  • setContent: setContent 的作用是和开发 Activity 中使用的 setContentView 功能是一样的,通过 content 传入 @Composable
    标记的方法来构建 UI。

运行后就可以在手机上看到 Hello Android 的文字,除了用 Text 来表现文字的显示,Compose 还有对应的多种属性来改变控件的显示效果和丰富的控件来构建复杂的界面。

基础控件

Text

Text 类似于 Android View 的 TextView,同样它像 TextView 一样有很多的属性可以设置:

  • text : String:设置文字内容
  • modifier : Modifier:Text 的修饰符
  • color : Color:文字颜色的设置,可以通过使用 Compose 预先定义的如 Color.Blue 或者直接输入颜色值 Color(0xFF000000)
  • fontSize:TextUnit:设置字体大小,如 20.sp
  • fontFamily: FontFamily?:设置字体
  • fontWeight: FontWeight?:字体粗细
  • lineHeight: TextUnit:设置行高
  • letterSpacing:TextUnit:设置字符间距
  • textDecoration : TextDecoration?:设置删除线和下划线
  • maxLine : Int:最大显示的行数
  • fontStyle : FontStyle?:设置字体类型,如 FontStyle.Italic
  • textAlign:TextAlign?:显示样式,如 TextAlign.Left
  • onTextLayout: (TextLayoutResult) -> Unit:文本计算完成回调
  • overflow: TextOverflow:文本溢出样式

示例

          Text(
                text = "Hello BillionBottle",
                modifier = Modifier.padding(5.dp),
                color = Color.Blue,
                textAlign = TextAlign.Start,
                textDecoration = TextDecoration.LineThrough,
                fontStyle = FontStyle.Italic,
                maxLines = 1
            )

效果:
text

Button

Button 主要是用来响应用户的点击事件的,它主要有以下属性:

  • onClick : () -> Unit:按钮点击时会进行回调
  • modifier : Modifier:Button 的修饰符
  • enabled : Boolean:设置按钮的有效性,默认是 true
  • shape: Shape:调整按钮的样子,默认是 MaterialTheme.shapes.small
  • border: BorderStroke?:设置按钮的外边框,如 CutCornerShape(30) 切角形状; RoundedCornerShape(50) 圆角形状
  • elevation: ButtonElevation?:设置按钮在Z轴方向上的高度
  • contentPadding: PaddingValues:内容与边界的距离
  • colors: ButtonColors:设置按钮的颜色,包括设置 enable/disable 的背景和 content 的颜色
  • content: @Composable () -> Unit:为 Button 设置内容,需要传入 @Compose 方法

示例

  Button(
  onClick = {},
  modifier = Modifier.padding(12.dp),
  colors = ButtonDefaults.buttonColors(
      backgroundColor = Color.Green,
      contentColor = Color.Blue
  ),
  elevation = ButtonDefaults.elevation(
      defaultElevation = 12.dp,
      pressedElevation = 12.dp
  ),
  border = BorderStroke(width = 1.dp, color = Color.Blue)
) {
  Text(text = "BillionBottle")
}

效果:

button

Image

Image 对应于 Android View 的 ImageView,可以用来显示图片,它主要有以下属性:

  • bitmap: ImageBitmap:可以直接传入 ImageBitmap 构建,如想显示 drawable
    文件夹下的图片,可以通过 var imageBitmap = ImageBitmap.imageResource(id = R.drawable.xxx)
  • contentDescription: String?:accessibility services 可以读取识别
  • modifier : Modifier:Image 的修饰符
  • aligment : Aligment:对齐方式
  • contentScale : ContentScale:图片的显示模式
  • alpha : Float:设置透明度,默认是 1.0f
  • colorFilter : ColorFilter:可以设置颜色滤镜

示例

   // 使用 drawable 下的图片资源显示图片
Image(
    painter = painterResource(R.drawable.xxx),
    contentDescription = "",
)

Surface

当想要为我们自定义的一个组件添加背景颜色时,我们就需要用到 Surface,它主要有以下属性:

  • modifier: Modifier:可以为 Surface 设置修饰符
  • shape: Shape:设置形状,默认是 RectangleShape
  • color: Color:设置背景色
  • contentColor: Color:为 Surface 中的 Text 文字设置颜色,当 Text 没有指定颜色时,就是使用该颜色
  • border: Border?:设置外边框
  • elevation: Dp:为 Surface 设置在 Z 轴方向上的高度
  • content: @Composable () -> Unit:为 Surface 设置内容布局,需要传入 @Compose 方法

示例


    Surface(modifier = Modifier.padding(4.dp), color = Color.Gray) {
        Column {
            Text(modifier = Modifier.align(Alignment.CenterHorizontally), text = "custom")
            Image(
                modifier = Modifier.size(150.dp),
                painter = ColorPainter(color = Color.Green),
                contentDescription = "image color"
            )
        }
    }

效果:
surface

Canvas

Canvas 是在屏幕上指定区域执行绘制的组件。注意在使用时需要添加修饰符来指定尺寸,可以通过 Modifier.size
设置固定的大小,也可以使用 Modifier.fillMaxSizeColumnScope.weight 设置相对父组件大小。如果父组件没有设置大小,那么 Canvas
必须要设置固定的大小。

Canvas 就是类似于原来的自定义 View,但是更加的简便,通过 DrawScope 定义的绘制方法进行绘制出自己想要的效果,可以通过 drawArcdrawCircle
drawLinedrawPoints 等方法来绘制图形(详情可参考 DrawScope 下的方法):



Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    // 绘制一条从左下角到右上角的蓝色的线
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )

    // 在以 200,1200 位置 120 为半径绘制一个圆
    drawCircle(color = Color.Green, center = Offset(200f, 1200f), radius = 120f)
}

效果:

surface

布局控件

Compose 提供了一些可用的布局组件来使我们更好地对 UI 元素进行布局:

Column

Android 的 LinearLayout 控件想必对学习 Android 的人来说非常熟悉,而 Column 就是非常类似于 LinearLayout 设置竖向排列的布局方式。 观察它的声明方式


@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

Column 有两个属性可以控制 children 布局方式:
verticalArrangement 是控制子元素的垂直排列方式,默认是 Arrangement.Top,尽可能的靠近主轴顶部排列。它还有其他的几种取值来表示不同的布局方式:

  • Arrangement.BOTTOM:垂直排列并尽可能靠近底部
  • Arrangement.CENTER:垂直居中排列
  • Arrangement.SpaceBetween:均匀的分布子元素
  • Arrangement.SpaceEvenly:使得子元素同等间隔均分放置,但是元素的头和尾部都没有间隔
  • Arrangement.SpaceAround:使得子元素同等间隔均分放置,子元素的开头和结尾的间隔大小为中间的一半

horizontalAlignment 是控制子元素的水平排列方式,默认是 Alignment.Start 对于一般情况下是从左边开始的。Alignment
下面定义了很多排列的方式,适用于 Column 的主要有三种:

  • Alignment.Start:左对齐
  • Alignment.End:右对齐
  • Alignment.CenterHorizontally:水平居中对齐

那 Column 的子控件是怎么放进去的呢?其实它还有一个属性是 content,它是一个发出子界面元素的函数,里面包含了需要的子元素。

例如下面的例子就使用了水平右对齐和垂直底部对齐:

@Composable
fun columnColumn() {
       Column(
            // modifier 会在下面说明,主要是用来扩展控件的功能如添加边距,宽高等    
            modifier = Modifier.height(100.dp).padding(5.dp),
            verticalArrangement = Arrangement.Bottom,
            horizontalAlignment = Alignment.End
        ) {
            Text("安卓")
            Text("BillionBottle")
        }
}

效果:

column

Row

与 Column 不同的是,Row 是以水平方向进行布局的,非常类似于 LinearLayout 设置水平排列的布局方式,Row
也有两个属性来表示水平和垂直方向上的排列方式,它的属性和使用方式也是非常类似于 Column。


@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
) {
    val measurePolicy = rowMeasurePolicy(horizontalArrangement, verticalAlignment)
    Layout(
        content = { RowScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

观察它的声明方式可以清晰的看出两者的控制水平和垂直方向的方式都是一致的,Arrangement 主要针对的是它的主轴方向(对于 Column 是垂直方向,对于 Row
则是水平方向), Alignment 就是另一个方向的排列,具体就不细说了,通过一个例子来看是如何使用的吧:


@Composable
fun rowShow() {
    // 创建了一个宽 200 dp,垂直方向上居中,水平对齐的布局
    Row(
        modifier = Modifier.width(200.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Start
    ) {
        Text("安卓")
        Text("BillionBottle")
    }
}

效果:

row

Box

使用 Box 可以将一个元素叠加放到另一个元素上面,类似于 FrameLayout 布局。查看 Box 的声明和相关属性:

@Composable
inline fun Box(
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    propagateMinConstraints: Boolean = false,
    content: @Composable BoxScope.() -> Unit
) {
    val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
    Layout(
        content = { BoxScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

其中 modifiercontent 和前面是一样的,contentAlignment 则是控制 Box
子元素的对齐方式,它有很多种方式,如可以设置顶部或者底部居中的方式,详情可以查看 Alignment 静态属性下面有

// 2D Alignments.

注释相关的内容。

来看看如何使用的吧:


@Composable
fun boxLayout() {
    Box(
        contentAlignment = Alignment.BottomCenter,
        modifier = Modifier
            .width(100.dp)
            .height(50.dp)
            .padding(bottom = 10.dp),
    ) {

        Text("BillionBottle", modifier = Modifier.background(Color.Yellow))
        Text(
            "安卓",
            modifier = Modifier.background(color = Color.Gray)
        )
    }
}

效果:

box

修饰符

Compose 基本上对每一个组件提供了修饰符来扩展组件的功能,包括组件的宽高、无障碍信息、用户输入以及用户点击滚动的高级互动。修饰符主要由 Modifier
这个类进行创建的,它的调用方式是链式调用每一次调用玩就会返回自己。常用的属性由 backgroundheightoffset sizeclickable
等,详情可以参考官方文档 Modifier

需要注意的是通过不同的调用顺序可能在界面上会显示不同的效果:

   Column(
        modifier = Modifier
            .width(100.dp)
            .height(100.dp)
    ) {
        Text("BillionBottle")
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite",
            // 1
            modifier = Modifier
                .background(Color.Green)
                .size(ButtonDefaults.IconSize)
                .padding(2.dp)
        )
    }

通过预览界面 Build & Refresh 可以看出如果将 modifier 的 .size.padding 两个调用互换个位置这个 Icon 的大小就会由比较大的差别。

通过对各种控件的叠加组合和组合,就能够构造出我们想要的界面。而且对于原来的 Android View 存在的过多嵌套可能会有性能影响的问题,Compose
可以有效地处理嵌套布局,堪称设计复杂界面的绝佳工具。

来个例子

下面是一个自定义的控件,通过传入 name、image 和 content,显示不同的 userProfile :

// @DrawableRes 指明传入的image必须为drawable下的资源文件
@Composable
fun userProfile(name:String,content:String,desc:String = "",@DrawableRes image:Int) {
        // 添加边距
        Row(modifier = Modifier.padding(all = 8.dp)) {
            Image(
                painter = painterResource(image),
                contentDescription = desc,
                modifier = Modifier
                    .size(40.dp)
                    // 将图片裁剪成圆形
                    .clip(CircleShape)
            )

            // 添加 Image 和 Column 间距
            Spacer(modifier = Modifier.width(8.dp))

            Column {
                Text(text = name)
                Spacer(modifier = Modifier.height(4.dp))
                Text(text = content)
            }
        }
}

传入对应的参数后显示的效果:

custom

4.状态管理

所谓的状态,可以理解为某个值的变化可以是一个布尔值的改变或者是一个数组的变化,也可以从界面上理解成按钮文字、颜色的状态,而 Compose 是声明式的
UI,主要是根据状态的改变进行重组的,这个时候就需要加入状态并对相关的状态进行管理。

在 compose runtime 中有个可观测类型的对象 MutableState<T> ,可以通过 mutableStateOf 创建:

interface MutableState<T> : State<T> {
    override var value: T
}

// 这三种声明方式都是一样的
val state = remember { mutableStateOf("") }
var value by remember { mutableStateOf("") }
val (value, setValue) = remember { mutableStateOf("") }

remember 是将该状态存储在 Composition 中,当重组发生的时候会自动丢弃原先的对象转而使用改变状态后的值。只要对 MutableState 的 value
进行改变就会引起用到该状态的 composable 方法重组。不多说,来看看 state 是如何使用的:

//module
data class Info(var content:String)

@Composable
fun Greeting(name: String) {
    var info by remember { mutableStateOf(Info("")) }
    MyApplicationTheme {
        // A surface container using the 'background' color from the theme
        Surface(color = MaterialTheme.colors.background) {
            Column(modifier = Modifier.padding(16.dp)) {

                if (info.content.isNotEmpty()){
                    Text(text = info.content)
                }
                OutlinedTextField(
                    value = info.content,
                    onValueChange = {
                        info = Info(it)
                    },
                    label = { Text("title") }
                )
            }
        }
    }
}

功能很简单,就是在 OutlinedTextField
中用键盘输入的内容如果不为空就能够实时显示在上方,主要是通过 var info by remember { mutableStateOf(Info("")) } 来进行改变的,当 info
这个变量的引用发生了改变的时候,Compose 就会刷新使用到这个变量的组件,对应的组件状态也会发生改变,所以在使用 Compose 的时候我们只需要更新数据就可以了。

但是 remember 只能在重组的时候保存状态,一旦其他情况如屏幕旋转等 Configuration 发生改变的时候 remember
就无能为力了,这时候就需要使用 rememberSaveable。只要是 Bundle 类型的数据,rememberSaveable 就能够自动保存。 使用方式:

  • 只要是 Parcelize 类型的,和 remember 相同:
@Parcelize
data class Info(val content: String): Parcelable

var value by rememberSaveable { mutableStateOf(Info("")) }
  • MapSaver:
data class Info(val content: String)

val infoSaver = run {
    val nameKey = "content"
    mapSaver(
        save = { mapOf(nameKey to it.content) },
        restore = { Info(it[nameKey] as String) }
    )
}
@Composable
fun CityScreen() {
    var infoState = rememberSaveable(stateSaver = citySaver) {
        mutableStateOf(Info(""))
    }
    Column(modifier = Modifier.padding(16.dp)) {

        if (infoState.value.content.isNotEmpty())
            Text(text = infoState.value.content)

        OutlinedTextField(
            value = infoState.value.content,
            onValueChange = {
                 infoState.value = Info("$it")
            },
            label = { Text("title") }
        )
    }
}
  • ListSaver
data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    // 数组中保存的值和 City 中的属性是顺序对应的
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("", ""))
    }
}

当然,compose 也支持其他类型的 State:

  • LiveData
  • Flow
  • RxJava2

在使用其他的 State 类型前,必须转换为 State<T> 类型,这样 compose 才能识别出这个是状态值,需要根据这个值进行刷新 ui,例如使用 LiveData 就要在
Composable 方法使用它之前转换成 tate 类型,可以使用 LiveData<T>.observeAsState()

小结

本文仅简单地介绍了 Android Compose 的基本内容,更多丰富的内容和细节可以去官网查看。随着版本的不断更新,也不断会有新的功能被添加,等待大家去探索!

更多精彩请关注我们的公众号「百瓶技术」,有不定期福利呦!

百瓶技术
127 声望18 粉丝

「百瓶」App 技术团队官方账号。