Android Compose 的使用

百瓶技術發表於2021-12-20

公眾號名片
作者名片

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 的基本內容,更多豐富的內容和細節可以去官網檢視。隨著版本的不斷更新,也不斷會有新的功能被新增,等待大家去探索!

更多精彩請關注我們的公眾號「百瓶技術」,有不定期福利呦!

相關文章