Jetpack Compose 常用组件

5,652 阅读6分钟

Jetpack Compose 通过 @Composable 注解标示的函数(可组合函数)来描述界面上元素,为方便起见,统一称为 "组件"。

基本组件

Text

Text 文本组件,相当于原生View 中的 TextView

@Composable
fun TextStringDemo() {
    Text(
        text = stringResource(id = R.string.text_content).repeat(50),   // String 类型
        style = TextStyle(
            fontSize = 18.sp,
            color = Color.Blue,
            fontStyle = FontStyle.Normal,
            textAlign = TextAlign.Left
        ),
        fontWeight = FontWeight.Medium,
        overflow = TextOverflow.Ellipsis,
        maxLines = 5
    )
}

Text 显示富文本

@Composable
fun TextAnnotatedStringDemo() {
    val builder = AnnotatedString.Builder("Hello").apply {
        pushStyle(
            SpanStyle(
                color = Color.Red,
                fontSize = 24.sp,
                fontStyle = FontStyle.Normal
            )
        )
        append("World, ")
        pop()
        append("The new Android UI")
    }
    val text = builder.toAnnotatedString()
    Text(
        text = text,  // AnnotatedString类型
        fontSize = 22.sp, modifier = Modifier.clickable {
        }
    )
}

Button

Button 按钮组件,相当于原生View 中的 Button

@Composable
fun ButtonDemo() {
    Button(
        onClick = {
            println("button clicked")
        },
    ) {
        Text("我是按钮 button")
    }
}

@Composable
fun OutlinedButtonDemo() {
    val context = LocalContext.current
    OutlinedButton(
        onClick = {
            showToast(context, "button clicked")
        },
        border = BorderStroke(1.dp, Color.Red),
        colors = ButtonDefaults.outlinedButtonColors(backgroundColor = Color.Transparent),
        shape = RoundedCornerShape(50)
    ) {
        Text("OutlinedButton", color = Color.Red)
    }
}

@Composable
fun IconToggleButtonDemo() {
    val checkState = remember { mutableStateOf(true) }
    IconToggleButton(
        checked = checkState.value,
        onCheckedChange = {
            checkState.value = it
        }) {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = null,
            tint = if (checkState.value) {
                Color.Red
            } else {
                Color.Gray
            }
        )
    }
}

button.png

Modifier

Modifier 组件修饰器,通过 Modifier 可以修改组件的大小,形状,边距,边框,点击等

@Composable
fun TextStringDemo() {
    Text(
        text = stringResource(id = R.string.text_content).repeat(50),   // String 类型
        style = TextStyle(
            fontSize = 18.sp,
            color = Color.Blue,
            fontStyle = FontStyle.Normal,
            textAlign = TextAlign.Left
        ),
        fontWeight = FontWeight.Medium,
        overflow = TextOverflow.Ellipsis,
        maxLines = 5,
        modifier = Modifier
            .background(Color.LightGray)
            .padding(20.dp)
    )
}

Modifier.png

TextField

TextField 输入框组件,相当于 原生View 中的 EditText

@Composable
fun TextFieldDemo() {
    var text by remember { mutableStateOf("") }
    TextField(
        value = text,
        onValueChange = {
            text = it
        },
        placeholder = {
            Text("input password")
        },
        visualTransformation = PasswordVisualTransformation(),
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Done
        ),
        leadingIcon = {
            Icon(
                imageVector = Icons.Filled.Lock,
                contentDescription = null
            )
        }
    )
}

TextField.png

Image

Image 图片组件,相当于 原生View 中的 ImageView

@Composable
fun ImageDemo() {
    val context = LocalContext.current
    Image(
        modifier = Modifier
            .width(100.dp)
            .height(100.dp)
            .border(
                width = 1.5.dp,
                color = Color(0xFFDA8C1A),
                shape = CircleShape
            )
            .padding(3.dp)
            .clip(shape = CircleShape)
            .clickable {
                Toast
                    .makeText(context, "image clicked", Toast.LENGTH_LONG)
                    .show()
            },
        painter = painterResource(id = R.drawable.zoom),
        contentScale = ContentScale.Crop,
        contentDescription = null
    )
}

// 网络图片需要添加依赖
@Composable
fun NetworkImageDemo() {
    Image(
        modifier = Modifier
            .width(100.dp)
            .height(100.dp),
        contentScale = ContentScale.Crop,
        painter = rememberGlidePainter(
            request = "https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/1/9/168329d14a4d9f35~tplv-t2oaga2asx-image.image",
            fadeIn = true,
            previewPlaceholder = R.drawable.zoom
        ),
        contentDescription = "network image",
    )
}

Image.png

RadioButton

@Composable
fun RadioButtonDemo() {
    val labels = listOf("Android", "iOS", "Kotlin", "Java", "Jetpack Compose")
    val indexOfChecked = remember { mutableStateOf(-1) }
    Column {
        labels.forEachIndexed { index, value ->
            Row(
                modifier = Modifier
                    .padding(5.dp)
                    .selectableGroup()
            ) {
                RadioButton(
                    selected = indexOfChecked.value == index,
                    onClick = {
                        indexOfChecked.value = index
                    }
                )
                Spacer(modifier = Modifier.width(10.dp))
                Text(value)
            }
        }
    }
}

RadioButton.png

Switch

@Composable
fun SwitchDemo() {
    val checkedState = remember { mutableStateOf(false) }
    Row(verticalAlignment = Alignment.CenterVertically) {
        Switch(
            checked = checkedState.value,
            onCheckedChange = {
                checkedState.value = it
            }
        )
        Text(if (checkedState.value) "开启" else "关闭")
    }
}

Switch.png

ProgressIndicator

@Preview(showBackground = true)
@Composable
fun LinearProgressIndicatorDemo() {
    LinearProgressIndicator()
}

@Preview(showBackground = true)
@Composable
fun LinearProgressIndicatorDemo2() {
    var progress by remember {
        mutableStateOf(0.5f)
    }
    val animProgress by animateFloatAsState(targetValue = progress)
    Column(
        modifier = Modifier.padding(10.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        LinearProgressIndicator(
            progress = animProgress,
            color = Color.Red,
            modifier = Modifier
                .width(300.dp)
                .height(20.dp)
                .clip(shape = RoundedCornerShape(10.dp))
        )
        Spacer(modifier = Modifier.height(20.dp))
        Button(onClick = {
            if (progress < 1.0f) {
                progress += 0.1f
            }
        }) {
            Text(text = "increase")
        }
    }
}


@Preview(showBackground = true)
@Composable
fun CircularProgressIndicatorDemo2() {
    CircularProgressIndicator()
}

@Preview(showBackground = true)
@Composable
fun CircularProgressIndicatorDemo() {
    var progress by remember {
        mutableStateOf(0.5f)
    }
    val animProgress by animateFloatAsState(targetValue = progress)
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        CircularProgressIndicator(
            progress = animProgress,
            modifier = Modifier
                .height(100.dp)
                .width(100.dp),
            color = Color.Red,
            strokeWidth = 5.dp
        )
        Spacer(modifier = Modifier.height(20.dp))
        Button(onClick = {
            if (progress < 1f) {
                progress += 0.1f
            }
        }) {
            Text("Add")
        }
    }
}

ProgressIndicator.png

Divider

@Composable
fun DividerDemo() {
    Column {
        Text("Hello World,".repeat(50), maxLines = 2, modifier = Modifier.padding(15.dp))
        Divider(startIndent = 15.dp)
        Text("Hello World,".repeat(50), maxLines = 2, modifier = Modifier.padding(15.dp))
        Divider(startIndent = 15.dp)
        Text("Hello World,".repeat(50), maxLines = 2, modifier = Modifier.padding(15.dp))
        Divider(startIndent = 15.dp)

    }
}

Divider.png

布局组件

Box

Box 组件,相当于 原生View 中的 FrameLayout

@Preview(showBackground = true)
@Composable
fun BoxDemo() {
    Box(
        modifier = Modifier
            .width(200.dp)
            .height(200.dp)
            .background(color = Color.Red),
        contentAlignment = Alignment.Center

    ) {
        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(color = Color.Blue)
                .align(Alignment.TopStart)
        )
        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(color = Color.Green)
                .align(Alignment.TopEnd)
        )

        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(color = Color.Yellow)
                .align(Alignment.BottomEnd)
        )
    }
}

Box.png

Row

Row 行组件,相当于 原生View 中的 horizontal 的 LinearLayout

@Composable
fun RowDemo() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 10.dp),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(Color.Red)
        )

        Box(
            modifier = Modifier
                .width(60.dp)
                .height(60.dp)
                .background(Color.Green)
        )

        Box(
            modifier = Modifier
                .width(70.dp)
                .height(70.dp)
                .background(Color.Blue)
        )

        Box(
            modifier = Modifier
                .width(80.dp)
                .height(80.dp)
                .background(Color.Yellow)
        )

        Box(
            modifier = Modifier
                .width(90.dp)
                .height(90.dp)
                .background(Color.Cyan)
        )
    }
}

Row.png

Column

Column 行组件,相当于 原生View 中的 vertical 的 LinearLayout

@Composable
fun ColumnDemo() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 10.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(Color.Red)
        )

        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(Color.Green)
        )

        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(Color.Blue)
        )

        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(Color.Green)
        )

        Box(
            modifier = Modifier
                .width(50.dp)
                .height(50.dp)
                .background(Color.Blue)
        )
    }
}

Column.png

进阶组件

LazyColumn

LazyColumn 组件,适合加载多数据的列表,相当于原生View中的 RecyclerView

@Composable
fun LazyColumnDemo() {
    val context = LocalContext.current
    val list = listOf("Android", "iOS", "HTML5", "Linux", "Kotlin")
    val onClick: (String) -> Unit = {
        Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
    }
    LazyColumn {
        // 头部布局
        item {
            Image(
                painter = painterResource(id = R.drawable.zoom),
                contentDescription = null,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(150.dp),
                contentScale = ContentScale.Crop
            )
        }
        // 中间列表项布局
        items(list.size) { index ->
            ContactsItem(list[index], onClick)
        }
        // 尾部布局
        item {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp),
                contentAlignment = Alignment.Center
            ) {
                Text("加载更多")
            }
        }
    }
}

LazyColumn.png

Spacer

相当于原生 View 中的 space,显示为空白区域

@Preview
@Composable
fun SpacerDemo() {
    Column {
        Box(
            modifier = Modifier
                .background(color = Color.Red)
                .size(width = 100.dp, height = 100.dp)
        )

        Spacer(modifier = Modifier.height(20.dp))
        Box(
            modifier = Modifier
                .background(color = Color.Blue)
                .size(width = 100.dp, height = 100.dp)
        )
    }
}

Spacer.png

Card

@Composable
fun CardDemo() {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(20.dp),
        elevation = 10.dp,
    ) {
        Column {
            Image(
                modifier = Modifier.height(200.dp),
                painter = painterResource(id = R.drawable.bmw),
                contentScale = ContentScale.Crop,
                contentDescription = null
            )
            Text(
                "New BMW 3",
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(15.dp)
            )
        }
    }
}

Card.png

AnimatedVisibility

@Composable
fun AnimatedVisibilityDemo() {
    val show = remember {
        mutableStateOf(true)
    }
    Card(
        modifier = Modifier
            .padding(10.dp)
            .clickable {
                show.value = !show.value
            },
        shape = RoundedCornerShape(6.dp),
        elevation = 5.dp
    ) {
        Column(modifier = Modifier.padding(10.dp)) {
            Text(
                text = stringResource(R.string.text_content),
                style = TextStyle(fontSize = 14.sp)
            )
            Spacer(modifier = Modifier.height(10.dp))
            AnimatedVisibility(visible = show.value) {
                Image(
                    painter = painterResource(id = R.drawable.zoom),
                    contentDescription = null,
                    modifier = Modifier.fillMaxWidth(),
                    contentScale = ContentScale.Crop
                )
            }
        }
    }
}

Dialog

@Preview(showBackground = true)
@Composable
fun AlertDialogDemo() {
    val dialogState: MutableState<Boolean> = remember { mutableStateOf(false) }
    Button(
        onClick = {
            dialogState.value = true
        },
        modifier = Modifier
            .width(200.dp)
            .wrapContentHeight()
    ) {
        Text(text = "open dialog")
    }
    if (dialogState.value) {
        ShowAlertDialog({
            dialogState.value = false
        }, {
            dialogState.value = false
        })
    }
}


@Composable
fun ShowAlertDialog(confirm: () -> Unit, dismiss: () -> Unit) {
    AlertDialog(
        onDismissRequest = dismiss, // Executes when the user tries to dismiss the Dialog by clicking outside or pressing the back button. This is not called when the dismiss button is clicked
        title = {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Icon(
                    imageVector = Icons.Filled.Notifications,
                    contentDescription = null
                )
                Text(text = "我是标题")
            }
        },
        text = {
            Text("我是内容")
        },
        confirmButton = {
            TextButton(onClick = confirm) {
                Text(text = "确定")
            }
        },
        dismissButton = {
            TextButton(onClick = confirm) {
                Text("取消")
            }
        },
        properties = DialogProperties(
            dismissOnBackPress = false,
            dismissOnClickOutside = false,
            securePolicy = SecureFlagPolicy.SecureOn
        )
    )
}

AlertDialog.png

TabRow / ScrollableTabRow

TabRow 相当于原生View中的 TabLayout

TabRow: 包含一行 Tab, 其中的 Tab 均匀分布,每一个 Tab 占用相等的宽度

ScrollableTabRow: 可以滚动的 TabRow

@Composable
fun TabRowDemo() {
    val state = remember { mutableStateOf(0) }
    val titles = listOf<String>("推荐", "体育新闻", "Android软件工程师")
    Column {
        TabRow(selectedTabIndex = state.value) {
            titles.forEachIndexed { index, value ->
                Tab(
                    text = { Text(value) },
                    selected = state.value == index,
                    onClick = {
                        state.value = index
                    }
                )
            }
        }
        Spacer(modifier = Modifier.height(20.dp))
        Text(
            modifier = Modifier.align(Alignment.CenterHorizontally),
            text = "第${state.value}个Tab, ${titles[state.value]}",
            style = TextStyle(fontSize = 20.sp)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun ScrollableTabRowDemo() {
    val state = remember { mutableStateOf(0) }
    val titles = listOf<String>("推荐", "Kotlin 入门到精通", "Android软件工程师", "Web前端工程师")
    Column {
        ScrollableTabRow(
            selectedTabIndex = state.value,
            modifier = Modifier
                .fillMaxWidth(),
            edgePadding = 6.dp
        ) {
            titles.forEachIndexed { index, value ->
                Tab(
                    text = {
                        Text(
                            value,
                            fontSize = if (state.value == index) 18.sp else 14.sp
                        )
                    },
                    selected = state.value == index,
                    onClick = {
                        state.value = index
                    }
                )
            }
        }
        Spacer(modifier = Modifier.height(20.dp))
        Text(
            modifier = Modifier.align(Alignment.CenterHorizontally),
            text = "第${state.value}个Tab, ${titles[state.value]}",
            style = TextStyle(fontSize = 20.sp)
        )
    }
}

TabRow.png

HorizontalPager / VerticalPager

HorizontalPager 组件相当于原生View中的 ViewPager

@Preview(showBackground = true)
@ExperimentalPagerApi
@Composable
fun HorizontalPagerDemo2() {
    val pagerState = rememberPagerState(pageCount = 10)
    val images = listOf(
        R.drawable.zoom,
        R.drawable.link_co,
        R.drawable.honda,
        R.drawable.bmw,
        R.drawable.maserati
    )
    HorizontalPager(state = pagerState) { page ->
        Image(
            painter = painterResource(id = images[page % images.size]),
            contentDescription = null,
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp),
            contentScale = ContentScale.Crop
        )
    }
}



HorizontalPager 与 ScrollableTabRow 结合使用

@Preview
@ExperimentalPagerApi
@Composable
fun HorizontalPagerWithScrollableRow() {
    val datas = listOf(
        TabItem("马自达", R.drawable.zoom),
        TabItem("领克", R.drawable.link_co),
        TabItem("本田", R.drawable.honda),
        TabItem("宝马", R.drawable.bmw),
        TabItem("玛莎拉蒂", R.drawable.maserati)
    )
    val pagerState = rememberPagerState(pageCount = datas.size)
    val coroutineScope = rememberCoroutineScope()
    Column {
        ScrollableTabRow(
            selectedTabIndex = pagerState.currentPage,
            edgePadding = 5.dp,
            indicator = { tabPositions ->
                TabRowDefaults.Indicator(
                    modifier = Modifier
                        .pagerTabIndicatorOffset(
                            pagerState = pagerState,
                            tabPositions = tabPositions
                        )
                        .width(20.dp)
                )
            }

        ) {
            datas.forEachIndexed { index, item ->
                Tab(
                    selected = index == pagerState.currentPage,
                    onClick = {
                        coroutineScope.launch {
                            pagerState.animateScrollToPage(index)
                        }
                    },
                    modifier = Modifier
                        .height(40.dp)
                        .wrapContentWidth()
                ) {
                    Text(item.name)
                }
            }
        }
        HorizontalPager(state = pagerState) { page ->
            Image(
                painter = painterResource(id = datas[page % datas.size].resId),
                contentDescription = null,
                modifier = Modifier
                    .height(200.dp)
                    .fillMaxWidth(),
                contentScale = ContentScale.Crop
            )
        }

    }
}

tabrow_pager.png

DropdownMenu

DropdownMenu 相当于原生View中的 PopupWIndow

@Preview
@Composable
fun DropdownMenuDemo() {
    val expand = remember {
        mutableStateOf(false)
    }
    val context = LocalContext.current
    Row(
        horizontalArrangement = Arrangement.Center,
        modifier = Modifier.fillMaxWidth()
    ) {
        IconButton(onClick = { expand.value = true }) {
            Icon(Icons.Filled.Add, contentDescription = "")
        }
    }
    DropdownMenu(
        expanded = expand.value,
        onDismissRequest = { expand.value = false },
        modifier = Modifier
            .width(100.dp)
            .wrapContentHeight()
            .shadow(
                elevation = 1.dp,
                clip = false
            ),
        offset = DpOffset(200.dp, 0.dp)
    ) {
        DropdownMenuItem(onClick = { showToast(context, "分享") }) {
            Row {
                Icon(Icons.Filled.Share, contentDescription = null)
                Spacer(modifier = Modifier.width(6.dp))
                Text("分享")
            }
        }
        DropdownMenuItem(onClick = { showToast(context, "收藏") }) {
            Row {
                Icon(Icons.Filled.Favorite, contentDescription = null)
                Spacer(modifier = Modifier.width(6.dp))
                Text("收藏")
            }
        }
        DropdownMenuItem(onClick = { showToast(context, "导出") }) {
            Row {
                Icon(Icons.Filled.ExitToApp, contentDescription = null)
                Spacer(modifier = Modifier.width(6.dp))
                Text("导出")
            }
        }
    }
}

DropdownMenu.png

Slider

@Preview(showBackground = true)
@Composable
fun SliderDemo() {
    var slideValue by remember { mutableStateOf(0f) }
    Column(
        modifier = Modifier.fillMaxWidth(),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = slideValue.toString())
        Slider(
            value = slideValue,
            onValueChange = { slideValue = it },
            valueRange = 0f..5f,
            steps = 4   // 4步阶段,共 5 段
        )
    }
}

Slider.png

ModalBottomSheetLayout

@Preview(showBackground = true)
@ExperimentalMaterialApi
@Composable
fun ModalBottomSheetLayoutDemo() {
    val sheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
    val coroutineScope = rememberCoroutineScope()
    ModalBottomSheetLayout(sheetState = sheetState,
        sheetShape = RoundedCornerShape(10.dp),
        sheetContent = {
            Column(
                modifier = Modifier
                    .height(600.dp)
                    .fillMaxWidth()
            ) {
                Box(
                    modifier = Modifier
                        .height(50.dp)
                        .fillMaxWidth()
                        .padding(horizontal = 15.dp)
                ) {
                    Text(
                        text = "评论",
                        fontSize = 18.sp,
                        modifier = Modifier.align(Alignment.Center)
                    )
                    Icon(
                        Icons.Filled.Close,
                        contentDescription = "close",
                        modifier = Modifier
                            .align(Alignment.CenterEnd)
                            .clickable {
                                coroutineScope.launch {
                                    sheetState.hide()
                                }
                            }
                    )
                }
                LazyColumn {
                    items(100) { index ->
                        ListItem {
                            Text("sheet content $index")
                        }
                    }
                }
            }
        }) {
        Column {
            Text("title")
            Spacer(modifier = Modifier.height(20.dp))
            Button(onClick = {
                coroutineScope.launch {
                    sheetState.show()
                }
            }) {
                Text(text = "show")
            }
        }
    }
}

ModalSheetLayout.png

Canvas

Canvas 组件常用于自定组件。

@Preview(showBackground = true)
@Composable
fun CanvasDemo1() {
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp)
    ) {
        val canvasWidth = size.width
        val canvasHeight = size.height
        drawLine(
            brush = Brush.linearGradient(colors = listOf(Color.Red, Color.Blue, Color.Green)),
            start = Offset.Zero,
            end = Offset(canvasWidth, canvasHeight),
            strokeWidth = 10f
        )
    }
}

Canvas.png

自定义 Layout

自定义横向瀑布流

@Composable
fun HorizontalStaggeredGrid(
    modifier: Modifier = Modifier,
    row: Int,
    horizontalSpace: Dp = 15.dp,
    verticalSpace: Dp = 12.dp,
    content: @Composable () -> Unit
) {
    Layout(modifier = modifier, content = content) { measurables, constraints ->
        // 每行的宽度,初始都是 0
        val rowWidths = IntArray(row) { 0 }
        // 每行的高度,初始都是 0
        val rowHeights = IntArray(row) { 0 }
        // 遍历每个孩子,List<Measurable> 转换成 List<Placeable>
        val placeableList = measurables.mapIndexed { index, measurable ->
            // 测量每个孩子组件
            val placeable = measurable.measure(constraints)
            // 当前孩子所在行的索引
            val rowIndex = index % row
            // 当前行宽度累加
            rowWidths[rowIndex] += placeable.width + horizontalSpace.roundToPx()
            // 当前行高度取 当前高度和此孩子高度的最大值
            rowHeights[rowIndex] = max(rowHeights[rowIndex], placeable.height)
            // lambda 返回值
            placeable
        }
        // 此自定义组件的宽度
        val width = rowWidths.maxOrNull()
            ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))  // 限制在minWidth和maxWidth之间
            ?: constraints.minWidth  // 如果rowWidths 为null,则设置为minWidth
        // 此自定义组件的高度
        val height = rowHeights.sumOf { it + verticalSpace.roundToPx() }
            .minus(verticalSpace.roundToPx())  // 减去最后一行的 verticalSpace
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))

        // 记录每行顶部所在位置
        val rowY = IntArray(row) { 0 }
        // 第0行为 0, 从第一行开始,每行的顶部位置为 上一行顶部位置 + 上一行的高度
        for (rowIndex in 1 until row) {
            rowY[rowIndex] = rowY[rowIndex - 1] + rowHeights[rowIndex - 1] + verticalSpace.roundToPx()
        }
        layout(width, height) {
            // 记录每行当前横向位置x
            val rowX = IntArray(row) { 0 }
            placeableList.forEachIndexed { index, placeable ->
                // 所在行索引
                val rowIndex = index % row
                placeable.placeRelative(x = rowX[rowIndex], y = rowY[rowIndex])
                rowX[rowIndex] += placeable.width + horizontalSpace.roundToPx()
            }
        }
    }
}

调用示例

layout.png



data class Item(val text: String, val height: Dp)

@Composable
fun HorizontalStaggeredGridSample() {
    val list = listOf(
        Item("TextView", 10.dp),
        Item("Button", 20.dp),
        Item("GridView", 30.dp),
        Item("ListView", 60.dp),
        Item("JetpackCompose", 20.dp),
        Item("AndroidStudio", 60.dp),
        Item("Compose-Desktop", 20.dp)
    )
    HorizontalStaggeredGrid(
        modifier = Modifier
            .wrapContentHeight()
            .horizontalScroll(rememberScrollState())
            .background(color = Color(0xffcccccc)),
        row = 5
    ) {
        for (index in 0..50) {
            val item = list[index % list.size]
            val color = when (index % list.size) {
                0 -> Color(0xffa23f12)
                1 -> Color(0xffff3456)
                2 -> Color(0xff12ff12)
                3 -> Color(0xff1a34ff)
                4 -> Color(0xffefac1f)
                5 -> Color(0xffa2040f)
                else -> Color(0xfff9af6f)
            }
            Text(
                modifier = Modifier
                    .background(color = color)
                    .height(item.height),
                text = item.text
            )
        }
    }
}

常见问题

Clickable 禁用 Ripple 波纹

inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
    clickable(
        indication = null,
        interactionSource = remember { MutableInteractionSource() }) {
        onClick()
    }
}

TextField 修改 height 导致显示不全

自定义 BasicTextField


@Composable
fun CustomTextField(
    modifier: Modifier = Modifier,
    leadingIcon: (@Composable () -> Unit)? = null,
    trailingIcon: (@Composable () -> Unit)? = null,
    placeholderText: String = "",
    fontSize: TextUnit = MaterialTheme.typography.body2.fontSize
) {
    var text by rememberSaveable { mutableStateOf("") }
    BasicTextField(
        modifier = modifier
            .fillMaxWidth()
            .padding(horizontal = 6.dp),
        value = text,
        onValueChange = {
            text = it
        },
        singleLine = true,
        cursorBrush = SolidColor(MaterialTheme.colors.primary),
        textStyle = LocalTextStyle.current.copy(
            color = MaterialTheme.colors.onSurface,
            fontSize = fontSize
        ),
        decorationBox = { innerTextField ->
            Row(
                modifier = modifier,
                verticalAlignment = Alignment.CenterVertically
            ) {
                if (leadingIcon != null) leadingIcon()
                Spacer(modifier = Modifier.width(5.dp))
                Box(
                    modifier = Modifier
                        .weight(1f)
                        .fillMaxHeight(),
                    contentAlignment = Alignment.CenterStart
                ) {
                    if (text.isEmpty())
                        Text(
                            placeholderText,
                            style = LocalTextStyle.current.copy(
                                color = MaterialTheme.colors.onSurface.copy(alpha = 0.3f),
                                fontSize = fontSize
                            )
                        )
                    innerTextField()
                }
                if (trailingIcon != null) trailingIcon()
            }
        }
    )
}

调用用例:

CustomTextField(
                            leadingIcon = {
                                Icon(
                                    imageVector = Icons.Filled.Search,
                                    contentDescription = null,
                                    modifier = Modifier.size(20.dp),
                                    tint = LocalContentColor.current.copy(alpha = 0.3f)
                                )
                            },
                            trailingIcon = null,
                            modifier = Modifier
                                .clip(RoundedCornerShape(5.dp))
                                .background(
                                    color = Color.Red,
                                )
                                .width(200.dp)
                                .height(40.dp),
                            fontSize = 14.sp,
                            placeholderText = "Search"
                        )

BasicTextField.png