Jeptpack Compose 官網(wǎng)教程學(xué)習(xí)筆記(二)布局

布局

主要學(xué)習(xí)內(nèi)容

  • 如何使用 Material 組件可組合項(xiàng)
  • 什么是修飾符以及如何在布局中使用它們
  • 如何創(chuàng)建自定義布局
  • 何時可能需要固有特性

修飾符

借助Modifier脚线,可以修飾可組合項(xiàng)完慧。您可以更改其行為拼余、外觀夫凸,添加無障礙功能標(biāo)簽等信息照雁,處理用戶輸入秦叛,甚至添加高級互動(例如使某些元素可點(diǎn)擊晦溪、可滾動、可拖動或可縮放)挣跋。Modifier是標(biāo)準(zhǔn)的 Kotlin 對象三圆。您可以將它們分配給變量并重復(fù)使用,也可以將多個修飾符逐一串聯(lián)起來避咆,以組合這些修飾符舟肉。

下面我們通過實(shí)現(xiàn)下方的個人介紹布局進(jìn)行學(xué)習(xí)


d2c39f3c2416c321.png

首先實(shí)現(xiàn)最基礎(chǔ)的布局

@Preview(group = "2.1", widthDp = 160, backgroundColor = 0xFFFFFF, showBackground = true)
@Composable
fun UserInfoPreview() {
    StudyOfJetpackComposeTheme {
        UserInfoCard()
    }
}

@Composable
fun UserInfoCard(name: String, time: Long = 1000 * 60 * 60 * 3) {
    Row {
        Image(painter = painterResource(id = R.drawable.user), contentDescription = null)
        Column {
            Text(text = name)
            Text(text = "${TimeUnit.MILLISECONDS.toMinutes(time)} minutes ago")
        }
    }
}

很顯然僅完成布局和預(yù)計(jì)實(shí)現(xiàn)的效果相差甚遠(yuǎn),需要通過設(shè)置控件屬性和Modifier進(jìn)行對應(yīng)的修飾

@Composable
fun UserInfoCard(
    name: String = "oddly",
    time: Long = 1000 * 60 * 3
) {
    Row(modifier = modifier.padding(4.dp)) {
        Image(
            painter = painterResource(id = R.drawable.user), contentDescription = null,
            modifier = Modifier
                .size(50.dp)
                .clip(CircleShape)
                .align(Alignment.CenterVertically),
            //圖像居中放大直至填充滿控件
            contentScale = ContentScale.Crop
        )
        Column(
            modifier = Modifier
                .align(Alignment.CenterVertically)
                .padding(start = 6.dp)
        ) {
            Text(text = name, fontWeight = FontWeight.Bold)
            Text(
                text = "${TimeUnit.MILLISECONDS.toMinutes(time)} minutes ago",
                style = MaterialTheme.typography.body2.copy(fontSize = 12.sp),
                color = Color.Unspecified.copy(ContentAlpha.medium)
            )
        }
    }
}
image-20220509111209823.png

通過包含文本的 Column 上使用 Modifier.padding查库,從而在可組合項(xiàng)的 start 上添加一些空間路媚,用以分隔圖像和文本

某些布局提供了僅適用于它們本身及其布局特性的修飾符。例如樊销,Row 中的可組合項(xiàng)可以訪問適用的修飾符(來自 Row 內(nèi)容的 RowScope 接收者)整慎,例如 weightalign。這種作用域限制具備類型安全性围苫,因此您不可能會意外使用在其他布局中不適用的修飾符(例如裤园,weightBox 中就不適用),系統(tǒng)會將其視為編譯時錯誤加以阻止

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

通過Row的構(gòu)造函數(shù)可以看出我們傳入的函數(shù)為RowScope的擴(kuò)展函數(shù)剂府,RowScope中規(guī)定了Modifier適用的修飾符

interface RowScope {
 @Stable
 fun Modifier.weight(
     weight: Float,
     fill: Boolean = true
 ): Modifier

 @Stable
 fun Modifier.align(alignment: Alignment.Vertical): Modifier

 @Stable
 fun Modifier.alignBy(alignmentLine: HorizontalAlignmentLine): Modifier

 @Stable
 fun Modifier.alignByBaseline(): Modifier

 @Stable
 fun Modifier.alignBy(alignmentLineBlock: (Measured) -> Int): Modifier
}

修飾符的作用與 View 系統(tǒng)中 XML 屬性類似拧揽,但特定于作用域的修飾符具備的類型安全性可幫助您發(fā)現(xiàn)和了解可用且適用于特定布局的內(nèi)容。與之相比腺占,XML 布局并不總是清楚某個布局屬性是否適用于給定視圖

<!-- 以下設(shè)置不會報錯淤袜,只會警告:Invalid layout param in XXX -->
<!-- 可以在RelativeLayout中設(shè)置orientation -->
<!-- 可以在RelativeLayout的子控件中設(shè)置layout_weight -->
<RelativeLayout
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 android:background="@color/purple_200">
 <TextView
     android:layout_weight="1"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:text="Fucking World!" />
</RelativeLayout>

大多數(shù)可組合項(xiàng)都接受可選的修飾符參數(shù),以使其更加靈活湾笛,從而讓調(diào)用方能夠修改它們饮怯。如果您要創(chuàng)建自己的可組合項(xiàng),不妨考慮使用修飾符作為參數(shù)嚎研,將其默認(rèn)設(shè)置為 Modifier(即不執(zhí)行任何操作的空修飾符)蓖墅,并將其應(yīng)用于函數(shù)的根可組合項(xiàng)库倘。在本例中:

@Composable
fun UserInfoCard(
    modifier: Modifier = Modifier,
    name: String = "oddly",
    time: Long = 1000 * 60 * 3
) {
    Row(modifier) { ... }
}

按照Goolgle推薦,修飾符應(yīng)該被指定為函數(shù)的第一個可選參數(shù)

修飾符順序

串聯(lián)修飾符時請務(wù)必小心论矾,因?yàn)轫樞蚝苤匾挑妗S捎谛揎椃麜?lián)成一個參數(shù),所以順序?qū)⒂绊懽罱K結(jié)果

例1:給UserInfoCard添加一個背景顏色和padding

@Composable
fun UserInfoCard(
    modifier: Modifier = Modifier,
    name: String = "oddly",
    time: Long = 1000 * 60 * 3
) {
    Row(modifier = modifier
        .padding(4.dp)
        .background(Purple200)) {
    ...
    }
}
image-20220509112817914.png

可以看到實(shí)現(xiàn)的效果與想象的有些不一樣贪壳,padding不應(yīng)該是內(nèi)邊距嗎饱亿?怎么這里padding設(shè)置的是外邊距,因?yàn)?padding 是在 background 修飾符前面應(yīng)用的

如果我們在 background 后面應(yīng)用 padding 修飾符實(shí)現(xiàn)效果才是內(nèi)邊距

image-20220509113116014.png

串聯(lián)修飾符相當(dāng)于是在前一個修飾符的基礎(chǔ)上進(jìn)行操作闰靴,所以在Compose中沒有margin修飾符【串聯(lián)方式下作用和padding重復(fù)】

例2:添加圓角和背景

@Composable
fun UserInfoCard(
    modifier: Modifier = Modifier,
    name: String = "oddly",
    time: Long = 1000 * 60 * 3
) {
    Row(modifier = modifier
        .padding(2.dp)
        .background(Purple200)
        .clip(RoundedCornerShape(4.dp))
        .padding(4.dp)) {
    ...
    }
}

此時會發(fā)現(xiàn)圓角效果并沒有實(shí)現(xiàn)彪笼!

其實(shí)并不是沒有實(shí)現(xiàn),還是因?yàn)轫樞騿栴}導(dǎo)致background先進(jìn)行蚂且,然后進(jìn)行clip配猫,如果在clip后再新增一個background就可以看到clip實(shí)際上是有效的。

Row(modifier = modifier
    .padding(2.dp)
    .background(Purple200)
    //為了效果明顯杏死,修改圓角半徑大小
    .clip(RoundedCornerShape(12.dp))
    .background(Teal200)
    .padding(4.dp)) {
    ...
}
image-20220509114407857.png

clip約束的區(qū)域只對之后的修飾符生效

所以正確的順序應(yīng)該為:

Row(modifier = modifier
        .padding(2.dp)
        .clip(RoundedCornerShape(4.dp))
        .background(Purple200)
        .padding(4.dp)) {
    ...
    }

明確的順序可幫助您推斷不同的修飾符將如何相互作用泵肄。您可以將這一點(diǎn)與 View 系統(tǒng)進(jìn)行比較。在 View 系統(tǒng)中淑翼,您必須了解盒模型腐巢;在這種模型中,在元素的“外部”應(yīng)用外邊距玄括,而在元素的“內(nèi)部”應(yīng)用內(nèi)邊距冯丙,并且背景元素將相應(yīng)地調(diào)整大小。修飾符設(shè)計(jì)使這種行為變得明確且可預(yù)測惠豺,并且可讓您更好地進(jìn)行控制银还,以實(shí)現(xiàn)您期望的確切行為

槽位API

槽位 API 是 Compose 引入的一種模式,它在可組合項(xiàng)的基礎(chǔ)上提供了一層自定義設(shè)置洁墙,在本例中蛹疯,提供的是可用的 Material 組件可組合項(xiàng)。

Button為例:

對一個按鈕而言热监,可能會給按鈕設(shè)置圖標(biāo)捺弦、文字,而針對于圖標(biāo)可能又有屬性需要設(shè)置孝扛,如圖標(biāo)大小列吼,位置等信息,文字也可能需要字體大小苦始、字體樣式等信息要設(shè)置寞钥,如果這些都需要在Button中使用參數(shù)方式設(shè)置會導(dǎo)致參數(shù)爆炸,而且容易出現(xiàn)部分功能還無法實(shí)現(xiàn)

//偽代碼陌选,僅用于描述
Button(
    text = "Button",
    icon: Icon? = myIcon,
    textStyle = TextStyle(...),
    spacingBetweenIconAndText = 4.dp,
    ...
)

因此Compose添加了槽位理郑,槽位會在界面中留出空白區(qū)域蹄溉,讓開發(fā)者按照自己的意愿來填充

@Composable
fun Button(
    modifier: Modifier = Modifier,
    onClick: (() -> Unit)? = null,
    ...
    content: @Composable () -> Unit
)
Button 槽位

命名為 content 的 lambda 是最后一個參數(shù)。這樣您炉,我們就能使用尾隨 lambda 語法柒爵,以結(jié)構(gòu)化方式將內(nèi)容插入到 Button 中。

在更復(fù)雜的組件(如頂部應(yīng)用欄)中赚爵,Compose 會大量使用槽位

在構(gòu)建自己的可組合項(xiàng)時棉胀,您可以使用槽位 API 模式提高它們的可重用性

Material 組件

Compose 附帶內(nèi)置的 Material 組件可組合項(xiàng),我們可以用它們創(chuàng)建應(yīng)用冀膝。最高級別的可組合項(xiàng)是 Scaffold唁奢。

Scaffold

可以使用 Scaffold 實(shí)現(xiàn)具有基本 Material Design 布局結(jié)構(gòu)的界面。Scaffold 可以為最常見的頂層 Material 組件(例如 TopAppBar畸写、BottomAppBar驮瞧、FloatingActionButtonDrawer)提供槽位氓扛。使用 Scaffold 時枯芬,您可以確保這些組件能夠正確放置并協(xié)同工作

@Composable
fun Scaffold(
    modifier: Modifier = Modifier,
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    floatingActionButton: @Composable () -> Unit = {},
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    isFloatingActionButtonDocked: Boolean = false,
    drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
    drawerGesturesEnabled: Boolean = true,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerDefaults.Elevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    drawerScrimColor: Color = DrawerDefaults.scrimColor,
    backgroundColor: Color = MaterialTheme.colors.background,
    contentColor: Color = contentColorFor(backgroundColor),
    content: @Composable (PaddingValues) -> Unit
) 

可以看到在Scaffold中存在許多槽位topBarbottomBar采郎、snackbarHost

Scaffold API 中的所有參數(shù)都是可選的千所,但 @Composable (InnerPadding) -> Unit 類型的正文內(nèi)容除外:lambda 會接收內(nèi)邊距作為參數(shù)。這是應(yīng)該應(yīng)用于內(nèi)容根可組合項(xiàng)的內(nèi)邊距蒜埋,用于在界面上適當(dāng)限制各個項(xiàng)

@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
    Scaffold(modifier) { paddingValue ->
        Text(text = "Hi Scaffold", modifier = Modifier.padding(paddingValue))
    }
}

如果我們想使用含有界面主要內(nèi)容的 Column淫痰,應(yīng)該將修飾符應(yīng)用于 Column

為了提高代碼的可重用性和可測試性,我們應(yīng)該將其構(gòu)造為多個小的數(shù)據(jù)塊

@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
    Scaffold(modifier) { paddingValue ->
        BodyContent(Modifier.padding(paddingValue))
    }
}

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Text(text = "Hi Scaffold!")
        Text(text = "There is BodyContent")
    }
}

TopAppBar

通常情況下整份,Android 應(yīng)用中會顯示一個頂部應(yīng)用欄待错,其中包含有關(guān)當(dāng)前界面、導(dǎo)航和操作的信息

Scaffold 包含一個頂部應(yīng)用欄的槽位烈评,其 topBar 參數(shù)為 @Composable () -> Unit 類型火俄,這意味著我們可以用任何想要的可組合項(xiàng)填充該槽位

@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
    Scaffold(
        modifier,
        topBar = { TopBarContent() },
    ) { paddingValue ->
        BodyContent(Modifier.padding(paddingValue))
    }
}

@Composable
fun TopBarContent() {
    Text(text = "TopAppBar",style = MaterialTheme.typography.h3)
}

不過,與大多數(shù) Material 組件一樣讲冠,Compose 附帶一個 TopAppBar 可組合項(xiàng)瓜客,其包含用于標(biāo)題、導(dǎo)航圖標(biāo)和操作的槽位竿开。此外谱仪,它還包含一些默認(rèn)設(shè)置,可以根據(jù) Material 規(guī)范的建議(例如要在每個組件上使用的顏色)進(jìn)行調(diào)整否彩。

按照槽位 API 模式疯攒,我們希望 TopAppBartitle 槽位包含一個帶有界面標(biāo)題的 Text

@Composable
fun TopBarContent() {
    TopAppBar(
        title = {
            Text(text = "MaterialDesign控件")
        }
    )
}
image-20220509140812920.png

頂部應(yīng)用欄通常包含一些操作項(xiàng)。在本示例中列荔,我們將添加一個收藏夾按鈕敬尺。當(dāng)您覺得自己已學(xué)會一些內(nèi)容時称杨,可以點(diǎn)按該按鈕。Compose 還帶有一些您可以使用的預(yù)定義 Material 圖標(biāo)筷转,例如關(guān)閉姑原、收藏夾和菜單圖標(biāo)。

頂部應(yīng)用欄中的操作項(xiàng)槽位為 actions 參數(shù)呜舒,該參數(shù)在內(nèi)部使用 Row锭汛,因此系統(tǒng)會水平放置多個操作。如需使用某個預(yù)定義圖標(biāo)袭蝗,可結(jié)合使用 IconButton 可組合項(xiàng)和其中的 Icon

@Composable
fun TopBarContent() {
    TopAppBar(
        title = {
            Text(text = "MaterialDesign控件")
        },
        actions = {
            IconButton(onClick = { /* doSomething() */ }) {
                Icon(Icons.Filled.Favorite, contentDescription = null)
            }
        }
    )
}
image-20220509141128138.png

放置修飾符

每當(dāng)創(chuàng)建新的可組合項(xiàng)時唤殴,提高可組合項(xiàng)可重用性的一種最佳做法是使用默認(rèn)為 Modifiermodifier 參數(shù)

當(dāng)BodyContent 可組合項(xiàng)已經(jīng)接受一個修飾符作為參數(shù)。如果要為 BodyContent 再添加一些內(nèi)邊距到腥,應(yīng)該在什么位置放置 padding 修飾符朵逝?

  1. 將修飾符應(yīng)用于可組合項(xiàng)中唯一的直接子元素,以便所有對 BodyContent 的調(diào)用都會應(yīng)用額外的內(nèi)邊距:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(8.dp)) {
        Text(text = "Hi Scaffold!")
        Text(text = "Here is BodyContent")
    }
}
  1. 在調(diào)用可視需要添加額外內(nèi)邊距的可組合項(xiàng)時乡范,應(yīng)用修飾符:
@Composable
fun ScaffoldStudy(modifier: Modifier = Modifier) {
    Scaffold(modifier) { paddingValue ->
        BodyContent(Modifier.padding(paddingValue).padding(8.dp))
    }
}

確定在何處放置修飾符完全取決于可組合項(xiàng)的類型和用例

如果修飾符是可組合項(xiàng)的固有特性配名,則將其放置在內(nèi)部;如果不是晋辆,則放置在外部渠脉。即如果這個修改不對外開放的時候就在內(nèi)部進(jìn)行修改【方法1】

如果想要使用更多Material圖標(biāo),需要在項(xiàng)目的build.gradle文件中添加依賴

dependencies {
...
implementation "androidx.compose.material:material-icons-extended:$compose_version"
}

列表

使用列表

顯示項(xiàng)列表是應(yīng)用的常見模式瓶佳。Jetpack Compose 可讓您使用 ColumnRow 可組合項(xiàng)輕松實(shí)現(xiàn)此模式芋膘,但它還提供了僅應(yīng)用于編寫和布局當(dāng)前可見項(xiàng)的延遲列表【LazyColumnLazyRow

@Composable
fun SimpleList() {
    Column {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

默認(rèn)情況下,Column 不會處理滾動操作霸饲,某些項(xiàng)是看不到的为朋,因?yàn)樗鼈冊诮缑娣秶狻U執(zhí)砑?verticalScroll修飾符厚脉,以在 Column 內(nèi)啟用滾動:

@Composable
fun SimpleList() {
    val scrollState = rememberScrollState()
    Column(Modifier.verticalScroll(scrollState)) {
        repeat(100) {
            Text("Item #$it")
        }
    }
}

但在數(shù)據(jù)量大的情況下使用這種方式創(chuàng)建列表會造成性能問題习寸,可能會導(dǎo)致界面卡頓

延遲列表

Column 會渲染所有列表項(xiàng),甚至包括界面上看不到的項(xiàng)器仗,當(dāng)列表變大時融涣,這會造成性能問題。為避免此問題精钮,請使用 LazyColumn威鹿,它只會渲染界面上的可見項(xiàng),因而有助于提升性能轨香,而且無需使用 scroll 修飾符

Jetpack Compose中的LazyColumn等同于RecycleView

LazyColumn 具有一個 DSL忽你,用于描述其列表內(nèi)容。您將使用 items臂容,它會接受一個數(shù)字作為列表大小科雳。它還支持?jǐn)?shù)組和列表(如需了解詳情根蟹,請參閱列表文檔部分)。

@Composable
fun LazyList() {
    val scrollState = rememberLazyListState()

    LazyColumn(state = scrollState) {
        items(100) {
            Text("Item #$it")
        }
    }
}

列表中顯示圖像

Image 是一個可組合項(xiàng)糟秘,用于顯示位圖或矢量圖像简逮。如果圖像是遠(yuǎn)程獲取的,則該過程涉及的步驟會更多尿赚,因?yàn)閼?yīng)用需要下載資源散庶,將其解碼為位圖,最后在 Image 中進(jìn)行渲染凌净。

如需簡化這些步驟悲龟,可以使用 Coil 庫,它提供了能夠高效運(yùn)行這些任務(wù)的可組合項(xiàng)冰寻。

Coil:Android官推 kotlin-first的圖片加載庫

將 Coil 依賴項(xiàng)添加到項(xiàng)目的 build.gradle 文件中:

// build.gradle
dependencies {
    implementation 'io.coil-kt:coil-compose:1.4.0'
    ...
}

由于我們要提取遠(yuǎn)程圖像须教,請?jiān)谇鍐挝募刑砑?INTERNET 權(quán)限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.studyofjetpackcompose">
    
    <!-- AndroidManifest.xml -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        ...
        android:networkSecurityConfig="@xml/network_security_config">
        ...
    </application>
</manifest>

xml/network_security_config

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

代碼使用

const val imgPath="https://upload.jianshu.io/users/upload_avatars/22683414/11b218ff-7c9a-43dd-a84a-b3b41de13318.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240"

@Composable
fun LazyImageListItem(modifier: Modifier = Modifier, text: String) {
    Row(
        modifier = modifier
            .fillMaxWidth()
            .padding(4.dp)
    ) {
        Image(
            painter = rememberImagePainter(data = imgPath),
            contentDescription = null,
            modifier = Modifier.size(50.dp)
        )
        Text(text = "LazyItem #$text")
    }
}

@Composable
fun LazyImageList(modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(100) {
            LazyImageListItem(text="$it")
        }
    }
}

列表滾動控制

有時候我們需要控制列表滾動位置,比如滾動到最頂部或最底部

為避免在滾動時阻止列表呈現(xiàn)斩芭,滾動 API 屬于掛起函數(shù)轻腺。因此,我們需要在協(xié)程中調(diào)用它們秒旋。如需實(shí)現(xiàn)此目的约计,可使用 rememberCoroutineScope函數(shù)創(chuàng)建 CoroutineScope,從按鈕事件處理腳本創(chuàng)建協(xié)程

@Composable
fun LazyImageList(modifier: Modifier = Modifier, itemSize: Int) {
    val scrollState = rememberLazyListState()
    val coroutineScope = rememberCoroutineScope()
    Column(modifier) {
        Row {
            Button(modifier = Modifier.weight(1f), onClick = {
                coroutineScope.launch {
                    scrollState.animateScrollToItem(0)
                }
            }) {
                Text(text = "Scroll to Top")
            }
            Button(modifier = Modifier.weight(1f), onClick = {
                coroutineScope.launch {
                    scrollState.animateScrollToItem(itemSize - 1)
                }
            }) {
                Text(text = "Scroll to Bottom")
            }
        }
        LazyColumn(modifier = modifier, state = scrollState) {
            items(itemSize) {
                LazyImageListItem(text = "$it")
            }
        }
    }
}

自定義布局

Compose 提高了這些像小數(shù)據(jù)塊一樣的可組合項(xiàng)的可重用性迁筛,您可以組合各種內(nèi)置可組合項(xiàng)(例如 ColumnRowBox)耕挨,充分滿足某些自定義布局的需求

不過细卧,您可能需要為應(yīng)用構(gòu)建一些獨(dú)特內(nèi)容,構(gòu)建時需要手動測量和布置子元素筒占。對此贪庙,可以使用 Layout可組合項(xiàng)。實(shí)際上翰苫,ColumnRow 等所有較高級別的布局都是基于此構(gòu)建的

相當(dāng)于View系統(tǒng)中重寫ViewGroup編寫onMeasureonLayout函數(shù)止邮,不過在Compose中只需要編寫Layout可組合函數(shù)即可

其實(shí)是將測量和擺放在Layout中一起完成了而已,不過在測量上比View體系方便奏窑,不需要像View體系中手動計(jì)算margin值

Compose 中的布局原則

某些可組合函數(shù)在被調(diào)用后會發(fā)出一部分界面导披,這部分界面會添加到將呈現(xiàn)到界面上的界面樹中。每次發(fā)送(或每個元素)都有一個父元素埃唯,還可能有多個子元素撩匕。此外,它在父元素中具有位置 (x, y) 和大小墨叛,即 widthheight

即每個可組合函數(shù)創(chuàng)建一個控件都會記錄其的寬和高止毕、在父控件中的位置(x,y)

系統(tǒng)會要求元素使用其應(yīng)滿足的約束條件進(jìn)行自我測量模蜡。約束條件可限制元素的最小和最大 widthheight。如果某個元素有子元素扁凛,它可能會測量每個元素忍疾,以幫助確定它自己的大小。一旦某個元素報告了自己的大小谨朝,就有機(jī)會相對于自身放置它的子元素膝昆。創(chuàng)建自定義布局時,我們將對此做進(jìn)一步解釋說明

即讓最底層的控件先進(jìn)行測量得出寬高叠必,將數(shù)值傳給父控件荚孵,然后其父控件根據(jù)子控件的寬高計(jì)算得出自己的寬高再交給父控件的父控件,一層一層套娃向上傳遞

Compose 界面不允許多遍測量纬朝。這意味著收叶,布局元素不能為了嘗試不同的測量配置而多次測量任何子元素。單遍測量對性能有利共苛,使 Compose 能夠高效地處理較深的界面樹判没。如果某個布局元素測量了它的子元素兩次,而該子元素又測量了它的一個子元素兩次隅茎,依此類推澄峰,那么一次嘗試布置整個界面就不得不做大量的工作,這使得很難讓應(yīng)用保持良好的性能。不過备籽,有時除了子元素的單遍測量告知您的信息之外篷朵,您確實(shí)還需額外的信息 - 對于這些情況,我們稍后會介紹一些解決方法

布局修飾符

使用 layout 修飾符可手動控制如何測量和定位元素魂毁。通常,自定義 layout 修飾符的常見結(jié)構(gòu)如下:

fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
  ...
})

使用 layout 修飾符時出嘹,您會獲得兩個 lambda 參數(shù):

  • measurable:要測量和放置的子元素
  • constraints:子元素寬度和高度的最小值和最大值

Google Compose教程中使用firstBaselineToTop席楚。對于沒有了解過Text繪制中的FontMetrics的人而言可能理解起來會有些難度,所以這里的案例我選擇較為簡單的myPadding實(shí)現(xiàn)

image-20220509194935078.png

fun Modifier.myPadding(all: Dp,tag:String) =
    this.then(layout { measurable, constraints ->
        Log.i("Modifier", """
            constraints:${constraints}      
            measurable:${measurable}
            Tag=$tag
        """.trimIndent())
                      
        val placeable = measurable.measure(constraints)
        //將dp值轉(zhuǎn)化為px值【四舍五入】
        val padding = all.roundToPx()
        layout(placeable.width + padding * 2, placeable.height + padding * 2) {
            placeable.placeRelative(padding, padding)
        }
    })

@Composable
fun CustomSimpleLayout() {
    Text(
        text = "CustomSimpleLayout", modifier = Modifier
            .myPadding(10.dp,"1")
            .background(Purple200)
            //效果圖中沒有該修飾符
            .defaultMinSize(120.dp,60.dp)
            .myPadding(10.dp,"2")
    )
}

修飾符以串聯(lián)的方式進(jìn)行描述税稼,而layout返回一個Modifier烦秩,要和之前的Modifier串聯(lián)使用需要使用then修飾符

Logcat打印信息

I/Modifier: constraints:Constraints(minWidth = 0, maxWidth = 1080, minHeight = 0, maxHeight = 2069)      
    measurable:androidx.compose.ui.node.ModifiedDrawNode@32a3d5a
    Tag=1
    
I/Modifier: constraints:Constraints(minWidth = 330, maxWidth = 1080, minHeight = 165, maxHeight = 2069)      
    measurable:androidx.compose.ui.node.ModifiedDrawNode@549bb8b
    Tag=1
    
I/Modifier: constraints:Constraints(minWidth = 330, maxWidth = 1080, minHeight = 165, maxHeight = 2069)      
    measurable:androidx.compose.ui.semantics.SemanticsWrapper@b6468 id: 2 config: SemanticsConfiguration@de1a081{ Text : [CustomSimpleLayout] }
    Tag=2

通過打印對象可能比較好理解measurableconstraints,首先再將兩個參數(shù)描述抄過來

  • measurable:Measurable:要測量和放置的子元素
  • constraints:Constraints:子元素寬度和高度的最小值和最大值

constraints也就是控件的限制郎仆,從打印對象的描述中可以看出只祠,該參數(shù)就是控件size的范圍,可以通過defaultMinSize對最小寬高進(jìn)行設(shè)置

measurable只有在最后一個修飾符才會將要測量和放置的子元素加入丸升,因?yàn)樵诖税咐?code>myPadding修飾符修飾元素及其子元素只有Text铆农,由此計(jì)算得出實(shí)際上的控件大小

經(jīng)過測試只有在最后一個修飾符measurable才會是SemanticsConfiguration,具體的原因要查看布局流程

實(shí)際上的控件大小再經(jīng)過控件size范圍限制,最終得出顯示的控件大小

val placeable = measurable.measure(constraints)

因?yàn)橐獙?shí)現(xiàn)padding效果墩剖,我們需要將寬/高+外邊距值*2

于是這樣我們就完成了控件布局的第一步:測量大小猴凹,然后就是第二步:計(jì)算父控件中的相對擺放位置

很顯然在padding效果中x、y的偏差值就是外邊距值

最后通過調(diào)用 layout(width, height) 方法來指定其大小岭皂,在lambda中設(shè)置擺放位置

//指定控件大小
layout(placeable.width + padding * 2, placeable.height + padding * 2) {
    //設(shè)置擺放位置
    placeable.placeRelative(padding, padding)
}

創(chuàng)建自定義 LayoutLayoutModifier 時郊霎,Android Studio 會發(fā)出警告,直至系統(tǒng)調(diào)用 layout 函數(shù)

如果不調(diào)用 placeRelative爷绘,該可組合項(xiàng)將不可見书劝。placeRelative 會根據(jù)當(dāng)前的 layoutDirection 自動調(diào)整可放置位置

布局可組合項(xiàng)

上面的布局修飾符是針對于單個可組合項(xiàng)的測量和布局方式,但有時我們可能需要的是針對一組可組合項(xiàng)實(shí)施操作土至。為此购对,您可以使用 Layout 可組合項(xiàng)手動控制如何測量和定位布局的子元素。通常陶因,使用 Layout 的可組合項(xiàng)的常見結(jié)構(gòu)如下所示:

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

CustomLayout 至少需要 modifiercontent 參數(shù)骡苞;這些參數(shù)隨后會傳遞到 Layout

LayoutMeasurePolicy 類型)的尾隨 lambda 參數(shù)

  • measurables:List<Measurable>
  • constraints:Constraints

很顯然布局中可能不止存在一個子元素,所以這里通過List方式給出各個直接子元素與布局相關(guān)信息

constraints是針對修飾的布局而言的楷扬,所以只有一個

為了展示 Layout 的實(shí)際運(yùn)用解幽,讓我們使用 Layout 實(shí)現(xiàn)一個非常基本的 Column烘苹,以便了解該 API躲株。稍后會構(gòu)建更復(fù)雜的內(nèi)容,以展示 Layout 可組合項(xiàng)的靈活性镣衡。

實(shí)現(xiàn)MyColumn布局

自定義 MyColumn 實(shí)現(xiàn)類似Column會垂直布局各個項(xiàng)霜定,首先創(chuàng)建名為 MyColumn 的新可組合項(xiàng),然后添加 Layout 可組合項(xiàng)的常見結(jié)構(gòu):

@Composable
fun MyColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        //根據(jù)約束邏輯捆探,測量和擺放子元素
    }
}

和View系統(tǒng)中寫ViewGroup一樣然爆,第一步為測量子元素尺寸。與布局修飾符的工作原理類似黍图,在 measurables lambda 參數(shù)中,您可以通過調(diào)用 measurable.measure(constraints) 獲取所有可測量的 content

在測量子元素時奴烙,還應(yīng)根據(jù)每行的 widthheight計(jì)算得出布局的大兄弧:

@Composable
fun MyColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        var maxWidth = 0
        var height = 0

        val placeableList = measurables.map { measurable ->
            //測量每個元素
            measurable.measure(constraints = constraints).also { placeable ->
                maxWidth = max(maxWidth, placeable.width)
                height += placeable.height
            }
        }
        ...
    }
}

MyColumn這個案例中,沒有必要進(jìn)一步限制子元素切诀,所以直接通過measurable.measure(constraints) 就可以了

如果需要寫Grid布局揩环,以2列為例→則需要限制子元素的maxWidth為布局maxWidth的一半

最后,我們通過調(diào)用 placeable.placeRelative(x, y) 將子元素放置到界面上幅虑。垂直放置子元素丰滑,需要跟蹤放置子元素的 y 坐標(biāo)。MyColumn 的最終代碼如下所示:

@Composable
fun MyColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        var maxWidth = 0
        var height = 0
        var yPosition = 0

        val placeableList = measurables.map { measurable ->
            //測量每個元素
            measurable.measure(constraints = constraints).also { placeable ->
                maxWidth = max(maxWidth, placeable.width)
                height += placeable.height
            }
        }

        layout(maxWidth, height) {
            placeableList.forEach { placeable ->
                //擺放子元素至界面
                placeable.placeRelative(x = 0, y = yPosition)
                //記錄下一個子元素擺放的y坐標(biāo)
                yPosition += placeable.height
            }
        }
    }
}

使用

@Preview(
    group = "2.6"
)
@Composable
fun MyColumnPreview(modifier: Modifier=Modifier) {
    MyColumn(modifier = modifier.background(Teal200)) {
        //可是我還沒找到工作(?ω? )
        Text(text = " 〉光帧/⌒ヽ 不想上班")
        Text(text = " く/?〝 ⌒ヽ  ")
        Text(text = "  | 3 (∪ ̄]")
        Text(text = " く??? (∩ ̄]")
        Text(text = " ̄ ̄ ̄ ̄  ̄ ̄ ̄ ̄")

        Text(text = "∧_∧")
        Text(text = "(il′‐ω‐)ヘ")
        Text(text = "∩,,__⌒  つっ")
    }
}
image-20220509220056501.png

復(fù)雜的自定義布局

Layout 的基礎(chǔ)知識已介紹完畢褒墨。我們來創(chuàng)建一個更復(fù)雜的示例炫刷,以展示 API 的靈活性。我們要構(gòu)建自定義的 Material Study Owl 交錯網(wǎng)格郁妈,如下圖中間位置所示:

image.png

當(dāng)然對于這種布局方式浑玛,我們可以采用Column配合Row的方式完成實(shí)現(xiàn),不過在本案例中選擇通過自定義布局的方式完成

如果要讓布局可以以各種行數(shù)展示噩咪,可以使用參數(shù)作為想要在布局上展示的行數(shù)顾彰。由于該信息應(yīng)該是在調(diào)用布局時出現(xiàn)的,因此將其作為參數(shù)傳遞:

@Composable
fun RowGrid(
    modifier: Modifier = Modifier,
    row: Int = 3,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        //根據(jù)約束邏輯胃碾,測量和擺放子元素
    }
}

首先要做的是測量子元素涨享,同樣在RowGrid中不需要對子元素進(jìn)行限制。再根據(jù)子元素的寬高計(jì)算出布局的寬高

布局寬高邏輯:

RowGrid的寬 = 最長行的寬

RowGrid的高 = 每一行的高累加

行高 = 行中最高元素的高

fun Constraints.widthRange() = minWidth.rangeTo(maxWidth)
fun Constraints.heightRange() = minHeight.rangeTo(maxHeight)

@Composable
fun RowGrid(
    modifier: Modifier = Modifier,
    rowNum: Int = 3,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier
            //默認(rèn)支持橫向滾動
            .horizontalScroll(rememberScrollState())
            .background(Purple200),
        content = content
    ) { measurables, constraints ->
        //記錄每行的寬度
        val widthList = MutableList(rowNum) { 0 }
        //記錄每行的高度
        val heightList = MutableList(rowNum) { 0 }

        val placeableList = measurables.mapIndexed { index, measurable ->
            measurable.measure(constraints = constraints).also { placeable ->
                //該元素應(yīng)該在哪一行
                val row = index % rowNum
                heightList[row] = max(heightList[row], placeable.height)
                widthList[row] = widthList[row] + placeable.width
            }
        }

        //布局的寬 = 最長行的寬仆百,再將值限制在constraints范圍中
        val width = widthList.maxOrNull()
            ?.coerceIn(constraints.widthRange())
            ?: constraints.minWidth
        //布局的高 = 每一行的高累加厕隧,再將值限制在constraints范圍中
        val height = heightList
            .sumOf { it }
            .coerceIn(constraints.heightRange())

        layout(width, height) {
            ...
        }
    }
}

計(jì)算出布局的寬高之后,我們就需要在layout中通過計(jì)算子元素的(x,y)儒旬,擺放子元素的位置

layout(width, height) {
    val rowX = MutableList(rowNum) { 0 }
    val rowY = MutableList(rowNum) { 0 }
    for (i in 1 until rowNum) {
        //計(jì)算每行元素的y坐標(biāo)
        rowY[i] = rowY[i - 1] + heightList[i - 1]
    }

    placeableList.mapIndexed { index, placeable ->
        val row = index % rowNum
        placeable.placeRelative(rowX[row], rowY[row])
        //計(jì)算出該行下一個元素的x坐標(biāo)
        rowX[row] = rowX[row] + placeable.width
    }
}

使用

val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)

@Preview(group = "2.7")
@Composable
fun RowGridPreview(modifier: Modifier = Modifier) {
    RowGrid {
        topics.forEach {
            RowGridItem(
                Modifier
                    .padding(4.dp)
                    .clip(RoundedCornerShape(4.dp))
                    .background(Purple200.copy(alpha = .2f)), it
            )
        }
    }
}

@Composable
fun RowGridItem(modifier: Modifier = Modifier, str: String) {
    Row(modifier = modifier) {
        Image(
            painter = painterResource(id = R.drawable.user),
            contentDescription = null,
            modifier = Modifier.height(48.dp)
        )
        Text(
            text = str, modifier = Modifier
                .align(Alignment.CenterVertically)
                .padding(horizontal = 2.dp)
        )
    }
}
image-20220510112954749.png

因?yàn)樘砑恿?code>horizontalScroll(rememberScrollState())栏账,可以進(jìn)行橫向滾動

如果使用的是互動式預(yù)覽按鈕
image.png

或通過點(diǎn)按 Android Studio 運(yùn)行按鈕在設(shè)備上運(yùn)行應(yīng)用,您會看到如何水平滾動內(nèi)容栈源。

深入了解布局修飾符

修飾符允許您自定義可組合項(xiàng)的行為挡爵。您可以將多個修飾符串聯(lián)在一起,以組合修飾符甚垦。修飾符有多種類型茶鹃,但在此部分中重點(diǎn)介紹的是 LayoutModifier,因?yàn)樗鼈兛梢愿淖兘缑娼M件的測量和布局方式艰亮。

可組合項(xiàng)對其自己的內(nèi)容負(fù)責(zé)闭翩,并且父元素不能檢查或操縱該內(nèi)容,除非該可組合項(xiàng)的發(fā)布者公開了明確的 API 來執(zhí)行此操作迄埃。

同樣疗韵,可組合項(xiàng)的修飾符對其他修飾符而言是不透明,修飾符看不到其他修飾符具體操作侄非,只能在其他修飾符修飾的基礎(chǔ)上執(zhí)行自己的操作

分析修飾符

由于 ModifierLayoutModifier 是公共接口蕉汪,因此您可以創(chuàng)建自己的修飾符。

padding 是一個由實(shí)現(xiàn) LayoutModifier 接口的類提供支持的函數(shù)逞怨,而且該函數(shù)將替換 measure 方法者疤。PaddingModifier 是一個實(shí)現(xiàn) equals() 的標(biāo)準(zhǔn)類,因此可以在重組后比較該修飾符叠赦。

下面顯示的是有關(guān) padding 如何修改元素的大小和約束條件的源代碼:

@Stable
fun Modifier.padding(all: Dp) =
    this.then(
        PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
    )

private class PaddingModifier(
    val start: Dp = 0.dp,
    val top: Dp = 0.dp,
    val end: Dp = 0.dp,
    val bottom: Dp = 0.dp,
    val rtlAware: Boolean,
) : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {

        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()

        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))

        val width = constraints.constrainWidth(placeable.width + horizontal)
        val height = constraints.constrainHeight(placeable.height + vertical)
        return layout(width, height) {
            if (rtlAware) {
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
            } else {
                placeable.place(start.roundToPx(), top.roundToPx())
            }
        }
    }
}

元素的新 width 將是子元素的 width + 元素寬度約束條件的起始和結(jié)束內(nèi)邊距值

元素的新height 將是子元素的 height + 元素高度約束條件的上下內(nèi)邊距值

修飾符順序

串聯(lián)修飾符時順序非常重要驹马,因?yàn)檫@些修飾符會從前到后應(yīng)用于所修改的可組合項(xiàng),這意味著左側(cè)修飾符的測量值和布局會影響右側(cè)的修飾符∨蠢郏可組合項(xiàng)的最終大小取決于作為參數(shù)傳遞的所有修飾符

首先算利,修飾符會從左到右更新約束條件,然后從右到左返回大小寇蚊。下面我們通過一個示例來更好地了解這一點(diǎn):

@Preview(
    group = "2.8",
    showBackground = true,
    backgroundColor = 0xFFFFFF,
    widthDp = 232,
    heightDp = 232
)
@Composable
fun TestModifierPreview() {
    Row {
        Row(
            modifier = Modifier
                .background(Teal200)
                .size(200.dp)
                .padding(16.dp)
        ) {
            Box(modifier = Modifier
                .fillMaxSize()
                .background(Purple200)) {}
        }
    }
}

預(yù)覽中對于在外層的布局通過size設(shè)置尺寸是無效的笔时,會默認(rèn)填充整個界面

首先,我們更改背景仗岸,了解修飾符對界面有何影響允耿;接下來,將大小限制為 200.dp widthheight扒怖,最后應(yīng)用內(nèi)邊距

由于約束條件在鏈中是從左到右傳播的较锡,因此對要測量的 Row 的內(nèi)容采用的約束條件為:widthheight 的最大值和最小值都為 (200-16-16)=168dp 。即Box 的大小為 168x168 dp

因此盗痒,在 modifySize 鏈從右向左運(yùn)行后蚂蕴, Row 的最終大小為 200x200 dp

image-20220510143216022.png

如果我們更改修飾符的順序,先應(yīng)用內(nèi)邊距俯邓,然后再應(yīng)用大小骡楼,則會得到不同的界面:

@Preview(
    group = "2.8",
    showBackground = true,
    backgroundColor = 0xFFFFFF,
    widthDp = 232,
    heightDp = 232
)
@Composable
fun TestModifierPreview() {
    Row {
        Row(
            modifier = Modifier
                .padding(16.dp)
                .size(200.dp)
        ) {
            Box(modifier = Modifier
                .fillMaxSize()
                .background(Purple200)) {}
        }
    }
}

在這種情況下,Rowpadding 最初具有的約束條件將被強(qiáng)制轉(zhuǎn)換為 size 約束條件稽鞭,在該約束條件下進(jìn)行子元素d 測量鸟整。因此,對于最小和最大 width 以及 height朦蕴,Box 將被約束為 200dp 篮条。

隨著修飾符從右向左修改大小,padding 修飾符會將大小增大為 (200+16+16)x(200+16+16)=232x232吩抓,這也就是 Row 的最終大小

image-20220510143325954.png

布局方向

可以通過更改 LocalLayoutDirection CompositionLocal 來更改可組合項(xiàng)的布局方向

@Composable
fun RowGrid(
    modifier: Modifier = Modifier,
    rowNum: Int = 3,
    content: @Composable () -> Unit
) {
    //將布局方向修改為從左向右
    @Composable
fun RowGrid(
    modifier: Modifier = Modifier,
    rowNum: Int = 3,
    content: @Composable () -> Unit
) {
    //將布局方向修改為從左向右
    CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr){
        ...
    }    
}

layoutDirectionlayout 修飾符或 Layout 可組合項(xiàng)的 LayoutScope 的一部分涉茧,可以在layoutLayout中獲取layoutDirection

不過在使用 layoutDirection 時,應(yīng)使用 place 放置可組合項(xiàng)而不是placeRelative

val LocalLayoutDirection = staticCompositionLocalOf<LayoutDirection> {
    noLocalProvidedFor("LocalLayoutDirection")
}
enum class LayoutDirection {
    /**
     * Horizontal layout direction is from Left to Right.
     */
    Ltr,
    /**
     * Horizontal layout direction is from Right to Left.
     */
    Rtl
}

約束布局

ConstraintLayout 有助于依據(jù)可組合項(xiàng)的相對位置將它們放置到界面上疹娶,是使用多個 Row伴栓、ColumnBox 元素的替代方案。在實(shí)現(xiàn)對齊要求比較復(fù)雜的較大布局時雨饺,ConstraintLayout 很有用挣饥。

需要在項(xiàng)目的 build.gradle 文件中添加 Compose ConstraintLayout 依賴項(xiàng):

dependencies {
    implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc02"
}

Compose 中的 ConstraintLayout 支持 DSL

  • 使用 createRefs()(或 createRef())創(chuàng)建的引用ConstraintLayout 中的每個可組合項(xiàng)都需要有與之關(guān)聯(lián)的引用沛膳。
  • 約束條件是使用 constrainAs 修飾符提供的,該修飾符將引用作為參數(shù)汛聚,可讓您在主體 lambda 中指定其約束條件锹安。
  • 約束條件是使用 linkTo 或其他有用的方法指定的。
  • parent 是一個現(xiàn)有的引用,可用于指定對 ConstraintLayout 可組合項(xiàng)本身的約束條件叹哭。

從一個簡單的例子開始:

@Composable
fun ConstraintLayoutStudy(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier) {
        val (button, text) = createRefs()
        Button(onClick = { /*TODO*/ }, modifier = Modifier.constrainAs(button) {
            top.linkTo(parent.top,margin = 12.dp)
        }) {
            Text(text = "Button")
        }

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom,margin = 12.dp)
        })
    }
}
image-20220510152254347.png

此代碼使用 16.dp 的外邊距來約束 Button 頂部到父項(xiàng)的距離忍宋,同樣使用 16.dp 的外邊距來約束 TextButton 底部的距離

如果希望文本水平居中,可以使用 centerHorizontallyTo 函數(shù)將 Textstartend 均設(shè)置為 parent 的邊緣:

@Composable
fun ConstraintLayoutStudy(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier) {
        ...
        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom,margin = 12.dp)
            centerHorizontallyTo(parent)
        })
    }
}
image-20220510154818035.png
fun centerHorizontallyTo(other: ConstrainedLayoutReference) {
    linkTo(other.start, other.end)
}

fun linkTo(
    start: ConstraintLayoutBaseScope.VerticalAnchor,
    end: ConstraintLayoutBaseScope.VerticalAnchor,
    startMargin: Dp = 0.dp,
    endMargin: Dp = 0.dp,
    @FloatRange(from = 0.0, to = 1.0) bias: Float = 0.5f
) {
    this@ConstrainScope.start.linkTo(start, startMargin)
    this@ConstrainScope.end.linkTo(end, endMargin)
    tasks.add { state ->
        state.constraints(id).horizontalBias(bias)
    }
}

可以看到centerHorizontallyTo內(nèi)部調(diào)用了linkTo函數(shù)實(shí)現(xiàn)居中

DSL還支持創(chuàng)建guideline风罩、barrierchain輔助類用于幫助布局

屏障[Barrier]

有時候我們需要以多個組件做為擺放的基準(zhǔn)糠排,可以理解為將多個組件組合看作為一個組件,基于這個組件位置的基礎(chǔ)上進(jìn)行擺放

//限制顯示的界面大小
@Preview(group = "2.9",widthDp = 200,heightDp = 120)
@Composable
fun ConstraintLayoutPreview() {
    ConstraintLayoutStudy_Barrier()
}

@Composable
fun ConstraintLayoutStudy_Barrier(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier) {
        val (button, text, button2,box,box2) = createRefs()

        //創(chuàng)建屏障
        val startBarrier = createStartBarrier(button, text)
        val endBarrier = createEndBarrier(button, text)
        val topBarrier = createTopBarrier(button, text)
        val bottomBarrier = createBottomBarrier(button, text)

        //兩個Box用于顯示屏障的具體位置與大小
        Box(modifier = Modifier.constrainAs(box){
            start.linkTo(startBarrier)
            top.linkTo(topBarrier)
        }.background(Teal200.copy(.5f)).fillMaxSize()){}

        Box(modifier = Modifier.constrainAs(box2){
            end.linkTo(endBarrier)
            bottom.linkTo(bottomBarrier)
        }.background(Purple700.copy(.5f)).fillMaxSize()){}

        Button(onClick = {}, modifier = Modifier.constrainAs(button) {
            top.linkTo(parent.top, margin = 12.dp)
        }) {
            Text(text = "Button")
        }

        Text("Text", Modifier.constrainAs(text) {
            top.linkTo(button.bottom, margin = 12.dp)
            //相對于button水平居中擺放
            centerHorizontallyTo(button)
        })

        Button(onClick = {}, modifier = Modifier.constrainAs(button2) {
            start.linkTo(endBarrier)
            linkTo(topBarrier, bottomBarrier)
        }) {
            Text(text = "Button2")
        }
    }
}
image-20220510164256560.png

為了布局邏輯簡單超升,將text改為相對于button水平居中入宦,然后新建一個button2使得它相對于buttontext組合成的組件垂直居中,在其右邊擺放

通過Box背景可以很顯然的看出對于textbutton組成的屏障的大小與位置【背景顏色重疊部分】

centerHorizontallyTo源碼可以知道想要實(shí)現(xiàn)水平居中效果室琢,只需要調(diào)用linkTo函數(shù)傳入startend就可以了乾闰,同理垂直居中只需要調(diào)用linkTo函數(shù)傳入topbottom

創(chuàng)建Barrier還有createAbsoluteLeftBarriercreateAbsoluteRightBarrier是用于國際化適配,因?yàn)橛行┱Z言是從右到左排列的盈滴,如阿拉伯語

  • 屏障(以及所有其他輔助類)可以在 ConstraintLayout 的正文中創(chuàng)建涯肩,但不能在 constrainAs 內(nèi)部創(chuàng)建
  • linkTo 可用Guideline和Barrier進(jìn)行約束,就像運(yùn)用引用進(jìn)行約束一樣

準(zhǔn)則[Guideline]

相當(dāng)于為布局中設(shè)置了一條看不見的基準(zhǔn)線巢钓,控件可以根據(jù)該基準(zhǔn)線位置進(jìn)行位置擺放

@Composable
fun ConstraintLayoutStudy_Guideline(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier.background(Color.White)) {
        val text = createRef()
        val guidelineStart = createGuidelineFromStart(.5f)
        val guidelineTop = createGuidelineFromTop(.1f)
        Text(text = "Text", modifier = Modifier.constrainAs(text) {
            start.linkTo(guidelineStart)
            top.linkTo(guidelineTop)
        })
    }
}
image-20220510170722457.png

可以看到text擺放到了布局水平位置一半的右邊病苗,垂直位置0.1的下方

此案例中只使用了guideline根據(jù)百分比來設(shè)置它的位置,其實(shí)也可以根據(jù)偏移量來設(shè)置

//根據(jù)左側(cè)距離父布局偏移量來設(shè)置 guideline 位置
createGuidelineFromStart(offset: Dp)
//根據(jù)左側(cè)距離父布局的百分比來設(shè)置 guideline 位置
createGuidelineFromStart(fraction: Float)
//同Barrier一樣症汹,在國際化才使用
createGuidelineFromAbsoluteLeft(offset: Dp)
createGuidelineFromAbsoluteLeft(fraction: Float)

createGuidelineFromEnd(offset: Dp)
createGuidelineFromEnd(fraction: Float)
createGuidelineFromAbsoluteRight(offset: Dp)
createGuidelineFromAbsoluteRight(fraction: Float)

createGuidelineFromTop(offset: Dp)
createGuidelineFromTop(fraction: Float)

createGuidelineFromBottom(offset: Dp)
createGuidelineFromBottom(fraction: Float)

其實(shí)就是上下左右加上國際化

自定義維度

默認(rèn)情況下硫朦,系統(tǒng)將允許 ConstraintLayout 的子元素選擇封裝其內(nèi)容所需的大小。例如烈菌,這意味著當(dāng)文本過長時阵幸,可以超出界面邊界:

@Preview(group = "2.9", widthDp = 400, heightDp = 120)
@Composable
fun ConstraintLayoutPreview() {
    ConstraintLayoutStudy_Dimension()
}

@Composable
fun ConstraintLayoutStudy_Dimension(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier.background(Color.White)) {
        val text = createRef()
        val guidelineStart = createGuidelineFromStart(.5f)
        Text(text = "不逼就不努力的,即使逼了也不會努力多久的超級賤人", modifier = Modifier.constrainAs(text) {
            linkTo(guidelineStart,parent.end)
        })
    }
}
image-20220512094801581.png

選擇將text居中顯示在guidelineStart與父控件的end芽世,但是text的長度可以超出這個范圍挚赊。如果希望讓text限制這個范圍中換行顯示,就需要更改textwidth行為

@Composable
fun ConstraintLayoutStudy_Dimension(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier.background(Color.White)) {
        val text = createRef()
        val guidelineStart = createGuidelineFromStart(.5f)
        Text(text = "不逼就不努力的济瓢,即使逼了也不會努力多久的超級賤人", modifier = Modifier.constrainAs(text) {
            linkTo(guidelineStart,parent.end)
            width = Dimension.preferredWrapContent
        })
    }
}
image-20220512095319566.png

可用的Dimension

  • preferredWrapContent:布局大小根據(jù)內(nèi)容設(shè)置荠割,同時受到布局約束的限制。比如該例子中text內(nèi)容文本長度超過了200dp旺矾,但是因?yàn)槭艿?code>linkTo(guidelineStart,parent.end)的限制蔑鹦,所以textend最多只能等于parent.end

  • wrapContentDimension的默認(rèn)值,布局大小根據(jù)內(nèi)容設(shè)置箕宙,不受布局約束限制

  • fillToConstraints:布局大小就是布局約束限制的空間

  • preferredValue:布局大小是固定值嚎朽,同時受到布局約束的限制

  • value:布局大小是固定的值,不受布局約束的限制

此外柬帕,Dimension 還可組合設(shè)置布局大小

例如:width = Dimension.preferredWrapContent.atLeast(100.dp)可設(shè)置最小布局大小哟忍,同樣還有 atMost()可設(shè)置最大布局大小等狡门。

鏈[Chain]

Chain作用為將一系列子元素按照順序打包成一行或一列,創(chuàng)建Chain的api只有兩個:

  • createHorizontalChain:創(chuàng)建橫向的鏈
  • createVerticalChain:創(chuàng)建縱向的鏈

不過官方將這個 api 標(biāo)記為可以改進(jìn)的狀態(tài)锅很,可能后續(xù)會發(fā)生變化

// TODO(popam, b/157783937): this API should be improved
fun createHorizontalChain(
    vararg elements: ConstrainedLayoutReference,
    chainStyle: ChainStyle = ChainStyle.Spread
)

// TODO(popam, b/157783937): this API should be improved
fun createVerticalChain(
    vararg elements: ConstrainedLayoutReference,
    chainStyle: ChainStyle = ChainStyle.Spread
)

參數(shù):

  • elements:需要打包在一起的所有子元素引用
  • chainStyle:鏈的類型其馏,目前有三種類型
    • Spread:所有子元素平均分布在父布局空間中,默認(rèn)類型
    • SpreadInside:第一個和最后一個分布在鏈條的兩端爆安,其余子元素平均分布剩下的空
    • Packed:所有子元素打包在一起叛复,并放在鏈條的中間
@Preview(group = "2.9", widthDp = 240, heightDp = 120)
@Composable
fun ConstraintLayoutPreview() {
    ConstraintLayoutStudy_Chain()
}

@Composable
fun ConstraintLayoutStudy_Chain(modifier: Modifier = Modifier) {
    ConstraintLayout(modifier = modifier.background(Color.White)) {
        val (box1, box2, box3) = createRefs()
        val boxSize = 60.dp
        createHorizontalChain(box1, box2, box3, chainStyle = ChainStyle.Spread)

        Box(modifier = modifier
            .size(boxSize)
            .background(Color.Red)
            .constrainAs(box1) {}) {}

        Box(modifier = modifier
            .size(boxSize)
            .background(Color.Green)
            .constrainAs(box2) {}) {}

        Box(modifier = modifier
            .size(boxSize)
            .background(Color.Blue)
            .constrainAs(box3) {}) {}
    }
}
`chainStyle `為 `ChainStyle.Spread` 的效果
`chainStyle `為 `ChainStyle.SpreadInside` 的效果
`chainStyle `為 `ChainStyle.Packed` 的效果

解耦[ConstraintSet]

以上的示例都是通過內(nèi)嵌的方式指定約定條件,不過在某些情況下我們可能需要更改約定條件扔仓,此時就需要將約束條件和布局進(jìn)行分離解耦褐奥。例如:橫豎屏切換情況下約束條件的變化

對于這些情況,可以通過使用 ConstraintSet進(jìn)行解耦分離当辐,具體步驟如下:

  1. ConstraintSet 作為參數(shù)傳遞給 ConstraintLayout
  2. 使用 layoutId 修飾符將在 ConstraintSet 中創(chuàng)建的引用分配給可組合項(xiàng)
@Composable
fun ConstraintLayoutStudy_Decoupled(modifier: Modifier = Modifier) {
    BoxWithConstraints {
        val constraints = if (maxWidth < maxHeight) {
            decoupledConstraints(margin = 16.dp) // Portrait constraints
        } else {
            decoupledConstraints(margin = 32.dp) // Landscape constraints
        }
        ConstraintLayout(modifier = modifier, constraintSet = constraints) {
            Button(onClick = { /*TODO*/ }, modifier = Modifier.layoutId("button")) {
                Text(text = "Button")
            }
            Text("Text", Modifier.layoutId("text"))
        }
    }
}

fun decoupledConstraints(margin: Dp) = ConstraintSet {
    val button = createRefFor("button")
    val text = createRefFor("text")

    constrain(button) {
        top.linkTo(parent.top, margin = margin)
    }
    constrain(text) {
        top.linkTo(button.bottom, margin)
    }
}

其中layoutIdcreateRefFor中的只需要一一對應(yīng)抖僵,createRefFor參數(shù)為Any,可以不用拘泥于String

BoxWithConstraints中記錄測量數(shù)據(jù)缘揪,可以通過寬高比確定橫豎屏

使用ConstraintSet方式進(jìn)行解耦時耍群,ConstraintLayout布局內(nèi)部就不能通過createRefscreateRef方式創(chuàng)建引用

@Composable
inline fun ConstraintLayout(
    constraintSet: ConstraintSet,
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    animateChanges: Boolean = false,
    animationSpec: AnimationSpec<Float> = tween<Float>(),
    noinline finishedAnimationListener: (() -> Unit)? = null,
    noinline content: @Composable () -> Unit
)

@Composable
inline fun ConstraintLayout(
    modifier: Modifier = Modifier,
    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
    crossinline content: @Composable ConstraintLayoutScope.() -> Unit
)

從源碼中我們可以找到答案,兩個方法中lambda所在環(huán)境不同

約束布局參考文獻(xiàn)

https://blog.csdn.net/lbs458499563/article/details/120386772

固有特性

Compose 有一項(xiàng)規(guī)則找筝,即蹈垢,子元素只能測量一次,測量兩次就會引發(fā)運(yùn)行時異常袖裕。但是曹抬,有時需要先收集一些關(guān)于子元素的信息,然后再對其進(jìn)行測量急鳄。

可以借助固有特性谤民,您可以先查詢子元素,然后再進(jìn)行實(shí)際測量

對于可組合項(xiàng)疾宏,可以查詢intrinsicWidthintrinsicHeight

  • (min|max)IntrinsicWidth:給定高度张足,可以正確繪制內(nèi)容的最小/最大寬度
  • (min|max)IntrinsicHeight:給定寬度,可以正確繪制內(nèi)容的最小/最大高度

實(shí)際運(yùn)用

假設(shè)我們需要創(chuàng)建一個可組合項(xiàng)坎藐,該可組合項(xiàng)在屏幕上顯示兩個用分隔線隔開的文本

我們可以將兩個 Text 放在同一 Row 中为牍,并在其中最大程度地擴(kuò)展,另外在中間放置一個 Divider岩馍。我們需要將分隔線的高度設(shè)置為與最高的 Text 相同碉咆,粗細(xì)設(shè)置為 width = 1.dp

@Preview(group = "2.10", widthDp = 240, heightDp = 120)
@Composable
fun IntrinsicWidthPreview() {
    Scaffold {
        IntrinsicStudy()
    }
}

@Composable
fun IntrinsicStudy(modifier: Modifier = Modifier) {
    Row(modifier = modifier
        .background(Purple200)
        .height(IntrinsicSize.Min)) {
        Text(text = "Left", modifier = Modifier.weight(1f), textAlign = TextAlign.Start)
        Divider(
            modifier = Modifier
                .width(1.dp)
                .fillMaxHeight(),
            color = Color.Black
        )
        Text(text = "Right", modifier = Modifier.weight(1f), textAlign = TextAlign.End)
    }
}
image-20220512112204666.png

可以看到分隔線擴(kuò)展到整個界面,這并不是我們想要的效果蛀恩,分割線應(yīng)該和Text的高度一致才對

之所以出現(xiàn)這種情況疫铜,是因?yàn)?Row 會逐個測量每個子元素,并且 Text 的高度不能用于限制 Divider双谆。我們希望 Divider 以一個給定的高度來填充可用空間块攒。為此励稳,我們可以使用 height(IntrinsicSize.Min) 修飾符

height(IntrinsicSize.Min) 可將其子元素的高度強(qiáng)行調(diào)整為最小固有高度。由于該修飾符具有遞歸性囱井,因此它將查詢 Row 及其子元素的 minIntrinsicHeight

@Composable
fun IntrinsicStudy(modifier: Modifier = Modifier) {
    Row(modifier = modifier
        .background(Purple200)
        .height(IntrinsicSize.Min)) {
        Text(text = "Left", modifier = Modifier.weight(1f), textAlign = TextAlign.Start)
        Divider(
            modifier = Modifier
                .width(1.dp)
                .fillMaxHeight(),
            color = Color.Black
        )
        Text(text = "Right", modifier = Modifier.weight(1f), textAlign = TextAlign.End)
    }
}
image-20220512112302262.png

行的 minIntrinsicHeight 將作為其子元素的最大 minIntrinsicHeight。分隔線的 minIntrinsicHeight 為 0趣避,因?yàn)槿绻麤]有給出約束條件庞呕,它不會占用任何空間。因此程帕,Row 的 height 約束條件將為 Text 的最大 minIntrinsicHeight住练,而 Divider 會將其 height 擴(kuò)展為 Row 給定的 height 約束條件

然而這里使用height(IntrinsicSize.Max)效果一致,這里官網(wǎng)教程看的比較迷愁拭,之后在琢磨琢磨(@_@;)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末讲逛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子岭埠,更是在濱河造成了極大的恐慌盏混,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惜论,死亡現(xiàn)場離奇詭異许赃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)馆类,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門混聊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乾巧,你說我怎么就攤上這事句喜。” “怎么了沟于?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵咳胃,是天一觀的道長。 經(jīng)常有香客問我社裆,道長拙绊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任泳秀,我火速辦了婚禮标沪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘嗜傅。我一直安慰自己金句,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布吕嘀。 她就那樣靜靜地躺著违寞,像睡著了一般贞瞒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趁曼,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天军浆,我揣著相機(jī)與錄音,去河邊找鬼挡闰。 笑死乒融,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的摄悯。 我是一名探鬼主播赞季,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼奢驯!你這毒婦竟也來了申钩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤瘪阁,失蹤者是張志新(化名)和其女友劉穎撒遣,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罗洗,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡愉舔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了伙菜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片轩缤。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖贩绕,靈堂內(nèi)的尸體忽然破棺而出火的,到底是詐尸還是另有隱情,我是刑警寧澤淑倾,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布馏鹤,位于F島的核電站,受9級特大地震影響娇哆,放射性物質(zhì)發(fā)生泄漏湃累。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一碍讨、第九天 我趴在偏房一處隱蔽的房頂上張望治力。 院中可真熱鬧,春花似錦勃黍、人聲如沸宵统。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽马澈。三九已至瓢省,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痊班,已是汗流浹背勤婚。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留辩块,地道東北人蛔六。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像废亭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子具钥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容