布局
主要學(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í)
首先實(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)
)
}
}
}
通過包含文本的 Column
上使用 Modifier.padding
查库,從而在可組合項(xiàng)的 start
上添加一些空間路媚,用以分隔圖像和文本
某些布局提供了僅適用于它們本身及其布局特性的修飾符。例如樊销,Row
中的可組合項(xiàng)可以訪問適用的修飾符(來自 Row 內(nèi)容的 RowScope
接收者)整慎,例如 weight
或 align
。這種作用域限制具備類型安全性围苫,因此您不可能會意外使用在其他布局中不適用的修飾符(例如裤园,weight
在 Box
中就不適用),系統(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)) {
...
}
}
可以看到實(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)邊距
串聯(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)) {
...
}
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
)
命名為 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
驮瞧、FloatingActionButton
和 Drawer
)提供槽位氓扛。使用 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
中存在許多槽位
:topBar
、bottomBar
采郎、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 模式疯攒,我們希望 TopAppBar
的 title
槽位包含一個帶有界面標(biāo)題的 Text
:
@Composable
fun TopBarContent() {
TopAppBar(
title = {
Text(text = "MaterialDesign控件")
}
)
}
頂部應(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)
}
}
)
}
放置修飾符
每當(dāng)創(chuàng)建新的可組合項(xiàng)時唤殴,提高可組合項(xiàng)可重用性的一種最佳做法是使用默認(rèn)為 Modifier
的 modifier
參數(shù)
當(dāng)BodyContent
可組合項(xiàng)已經(jīng)接受一個修飾符作為參數(shù)。如果要為 BodyContent
再添加一些內(nèi)邊距到腥,應(yīng)該在什么位置放置 padding
修飾符朵逝?
- 將修飾符應(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")
}
}
- 在調(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 可讓您使用 Column
和 Row
可組合項(xiàng)輕松實(shí)現(xiàn)此模式芋膘,但它還提供了僅應(yīng)用于編寫和布局當(dāng)前可見項(xiàng)的延遲列表【LazyColumn
和LazyRow
】
@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)(例如 Column
、Row
或 Box
)耕挨,充分滿足某些自定義布局的需求
不過细卧,您可能需要為應(yīng)用構(gòu)建一些獨(dú)特內(nèi)容,構(gòu)建時需要手動測量和布置子元素筒占。對此贪庙,可以使用 Layout
可組合項(xiàng)。實(shí)際上翰苫,Column
和 Row
等所有較高級別的布局都是基于此構(gòu)建的
相當(dāng)于View系統(tǒng)中重寫
ViewGroup
編寫onMeasure
和onLayout
函數(shù)止邮,不過在Compose中只需要編寫Layout
可組合函數(shù)即可其實(shí)是將測量和擺放在
Layout
中一起完成了而已,不過在測量上比View體系方便奏窑,不需要像View體系中手動計(jì)算margin值
Compose 中的布局原則
某些可組合函數(shù)在被調(diào)用后會發(fā)出一部分界面导披,這部分界面會添加到將呈現(xiàn)到界面上的界面樹中。每次發(fā)送(或每個元素)都有一個父元素埃唯,還可能有多個子元素撩匕。此外,它在父元素中具有位置 (x, y) 和大小墨叛,即 width
和 height
即每個可組合函數(shù)創(chuàng)建一個控件都會記錄其的寬和高止毕、在父控件中的位置(x,y)
系統(tǒng)會要求元素使用其應(yīng)滿足的約束條件進(jìn)行自我測量模蜡。約束條件可限制元素的最小和最大 width
和 height
。如果某個元素有子元素扁凛,它可能會測量每個元素忍疾,以幫助確定它自己的大小。一旦某個元素報告了自己的大小谨朝,就有機(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)
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
通過打印對象可能比較好理解measurable
和constraints
,首先再將兩個參數(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)建自定義
Layout
或LayoutModifier
時郊霎,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
至少需要 modifier
和 content
參數(shù)骡苞;這些參數(shù)隨后會傳遞到 Layout
在
Layout
(MeasurePolicy
類型)的尾隨 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ù)每行的 width
和height
計(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 = "∩,,__⌒ つっ")
}
}
復(fù)雜的自定義布局
Layout
的基礎(chǔ)知識已介紹完畢褒墨。我們來創(chuàng)建一個更復(fù)雜的示例炫刷,以展示 API 的靈活性。我們要構(gòu)建自定義的 Material Study Owl
交錯網(wǎng)格郁妈,如下圖中間位置所示:
當(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)
)
}
}
如果使用的是互動式預(yù)覽按鈕因?yàn)樘砑恿?code>horizontalScroll(rememberScrollState())栏账,可以進(jìn)行橫向滾動
或通過點(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í)行自己的操作
分析修飾符
由于 Modifier
和 LayoutModifier
是公共接口蕉汪,因此您可以創(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
width
和 height
扒怖,最后應(yīng)用內(nèi)邊距
由于約束條件在鏈中是從左到右傳播的较锡,因此對要測量的 Row
的內(nèi)容采用的約束條件為:width
和 height
的最大值和最小值都為 (200-16-16)=168dp
。即Box
的大小為 168x168 dp
因此盗痒,在 modifySize
鏈從右向左運(yùn)行后蚂蕴, Row
的最終大小為 200x200 dp
如果我們更改修飾符的順序,先應(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)) {}
}
}
}
在這種情況下,Row
和 padding
最初具有的約束條件將被強(qiáng)制轉(zhuǎn)換為 size
約束條件稽鞭,在該約束條件下進(jìn)行子元素d 測量鸟整。因此,對于最小和最大 width
以及 height
朦蕴,Box
將被約束為 200dp
篮条。
隨著修飾符從右向左修改大小,padding
修飾符會將大小增大為 (200+16+16)x(200+16+16)=232x232
吩抓,這也就是 Row
的最終大小
布局方向
可以通過更改 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){
...
}
}
layoutDirection
是 layout
修飾符或 Layout
可組合項(xiàng)的 LayoutScope
的一部分涉茧,可以在layout
和Layout
中獲取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
伴栓、Column
和 Box
元素的替代方案。在實(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)
})
}
}
此代碼使用 16.dp
的外邊距來約束 Button
頂部到父項(xiàng)的距離忍宋,同樣使用 16.dp
的外邊距來約束 Text
到 Button
底部的距離
如果希望文本水平居中,可以使用 centerHorizontallyTo
函數(shù)將 Text
的 start
和 end
均設(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)
})
}
}
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
风罩、barrier
和chain
輔助類用于幫助布局
屏障[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")
}
}
}
為了布局邏輯簡單超升,將text
改為相對于button
水平居中入宦,然后新建一個button2
使得它相對于button
和text
組合成的組件垂直居中,在其右邊擺放
通過Box
背景可以很顯然的看出對于text
和button
組成的屏障的大小與位置【背景顏色重疊部分】
由
centerHorizontallyTo
源碼可以知道想要實(shí)現(xiàn)水平居中效果室琢,只需要調(diào)用linkTo
函數(shù)傳入start
與end
就可以了乾闰,同理垂直居中只需要調(diào)用linkTo
函數(shù)傳入top
與bottom
創(chuàng)建
Barrier
還有createAbsoluteLeftBarrier
和createAbsoluteRightBarrier
是用于國際化適配,因?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)
})
}
}
可以看到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)
})
}
}
選擇將text
居中顯示在guidelineStart
與父控件的end
芽世,但是text
的長度可以超出這個范圍挚赊。如果希望讓text
限制這個范圍中換行顯示,就需要更改text
的width
行為
@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
})
}
}
可用的Dimension
:
preferredWrapContent
:布局大小根據(jù)內(nèi)容設(shè)置荠割,同時受到布局約束的限制。比如該例子中text
內(nèi)容文本長度超過了200dp
旺矾,但是因?yàn)槭艿?code>linkTo(guidelineStart,parent.end)的限制蔑鹦,所以text
的end
最多只能等于parent.end
wrapContent
:Dimension
的默認(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) {}) {}
}
}
解耦[ConstraintSet]
以上的示例都是通過內(nèi)嵌的方式指定約定條件,不過在某些情況下我們可能需要更改約定條件扔仓,此時就需要將約束條件和布局進(jìn)行分離解耦褐奥。例如:橫豎屏切換情況下約束條件的變化
對于這些情況,可以通過使用 ConstraintSet
進(jìn)行解耦分離当辐,具體步驟如下:
- 將
ConstraintSet
作為參數(shù)傳遞給ConstraintLayout
- 使用
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)
}
}
其中layoutId
與createRefFor
中的只需要一一對應(yīng)抖僵,createRefFor
參數(shù)為Any
,可以不用拘泥于String
BoxWithConstraints
中記錄測量數(shù)據(jù)缘揪,可以通過寬高比確定橫豎屏
使用
ConstraintSet
方式進(jìn)行解耦時耍群,ConstraintLayout
布局內(nèi)部就不能通過createRefs
或createRef
方式創(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)疾宏,可以查詢intrinsicWidth
或intrinsicHeight
:
-
(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)
}
}
可以看到分隔線擴(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)
}
}
行的 minIntrinsicHeight
將作為其子元素的最大 minIntrinsicHeight
。分隔線的 minIntrinsicHeight
為 0趣避,因?yàn)槿绻麤]有給出約束條件庞呕,它不會占用任何空間。因此程帕,Row 的 height
約束條件將為 Text
的最大 minIntrinsicHeight
住练,而 Divider
會將其 height
擴(kuò)展為 Row 給定的 height
約束條件
然而這里使用
height(IntrinsicSize.Max)
效果一致,這里官網(wǎng)教程看的比較迷愁拭,之后在琢磨琢磨(@_@;)