Thành phần và bố cục Material

Jetpack Compose cung cấp cách triển khai Material Design, một hệ thống thiết kế toàn diện để tạo các giao diện số. Các thành phần Material (nút, thẻ, công tắc, v.v.) và các bố cục như Scaffold có sẵn dưới dạng hàm có khả năng kết hợp.

Thành phần Material là các khối xây dựng tương tác để tạo giao diện người dùng. Compose đề xuất một số thành phần ngay từ đầu. Để biết những thành phần đã có sẵn, hãy xem Tài liệu tham khảo API Material trong công cụ Compose.

Thành phần Material sử dụng các giá trị do MaterialTheme cung cấp trong ứng dụng của bạn:

@Composable
fun MyApp() {
    MaterialTheme {
        // Material Components like Button, Card, Switch, etc.
    }
}

Để tìm hiểu thêm về chủ đề này, hãy xem hướng dẫn về Hệ thống thiết kế trong Compose.

Các vị trí nội dung

Các thành phần Material hỗ trợ nội dung bên trong (nhãn văn bản, biểu tượng, v.v.) có xu hướng cung cấp "các khe" — tức các hàm lambda chung chấp nhận nội dung của thành phần kết hợp — cũng như các hằng số công khai, như kích thước và khoảng đệm, để hỗ trợ sắp xếp nội dung bên trong cho phù hợp với các thông số kỹ thuật của Material.

Ví dụ: đây là Button:

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Hình 1. Button sử dụng khe content và khoảng đệm mặc định (bên trái) và Button sử dụng khe content để cung cấp một contentPadding tuỳ chỉnh (bên phải).

Button có một khe hàm lambda tạo vệt content chung, sử dụng một RowScope để sắp xếp bố cục nội dung các thành phần kết hợp trong một hàng. Phương thức này cũng có một thông số contentPadding để áp dụng khoảng đệm cho nội dung bên trong. Bạn có thể sử dụng các hằng số được cung cấp thông qua ButtonDefaults, hoặc các giá trị tuỳ chỉnh.

Ví dụ khác là ExtendedFloatingActionButton:

ExtendedFloatingActionButton(
    onClick = { /* ... */ },
    icon = {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite"
        )
    },
    text = { Text("Like") }
)

Hình 2. ExtendedFloatingActionButton sử dụng các khe icontext.

Thay vì một hàm lambda content chung, ExtendedFloatingActionButton có hai khe dành cho một nhãn icontext. Mặc dù mỗi khe đều hỗ trợ nội dung thành phần kết hợp chung, nhưng thành phần này vẫn có chủ đích về cách sắp xếp các nội dung bên trong. Tính năng này xử lý khoảng đệm, căn chỉnh và kích thước bên trong.

Scaffold

Compose cung cấp các bố cục thuận tiện cho việc kết hợp các thành phần Material vào các mẫu màn hình phổ biến. Các thành phần kết hợp như Scaffold cung cấp các khe cho nhiều loại thành phần và các thành phần màn hình khác.

Nội dung trên màn hình

Scaffold có một khe hàm lambda tạo vệt content chung. Hàm lambda nhận một phiên bản PaddingValues sẽ được áp dụng cho nội dung gốc — ví dụ: thông qua Modifier.padding — để bù vào các thanh trên cùng và dưới cùng, nếu chúng tồn tại.

Scaffold(/* ... */) { contentPadding ->
    // Screen content
    Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
}

Thanh ứng dụng

Scaffold cung cấp các khe cho thanh ứng dụng trên cùng hoặc thanh ứng dụng dưới cùng. Vị trí của các thành phần kết hợp được xử lý nội bộ.

Bạn có thể sử dụng khe topBarTopAppBar:

Scaffold(
    topBar = {
        TopAppBar { /* Top app bar content */ }
    }
) {
    // Screen content
}

Bạn có thể sử dụng khe bottomBarBottomAppBar:

Scaffold(
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

Bạn có thể dùng các khe này cho các thành phần Material khác như BottomNavigation. Bạn cũng có thể dùng các thành phần kết hợp tuỳ chỉnh — ví dụ: hãy xem màn hình giới thiệu từ mẫu Owl.

Các nút hành động nổi

Scaffold cung cấp khe cho nút hành động nổi.

Bạn có thể sử dụng khe floatingActionButtonFloatingActionButton:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    }
) {
    // Screen content
}

Vị trí dưới cùng của thành phần kết hợp FAB được xử lý nội bộ. Bạn có thể sử dụng thông số floatingActionButtonPosition để điều chỉnh vị trí theo chiều ngang:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to FabPosition.End
    floatingActionButtonPosition = FabPosition.Center
) {
    // Screen content
}

Nếu đang sử dụng khe bottomBar của thành phần kết hợp Scaffold, bạn có thể sử dụng thông số isFloatingActionButtonDocked để xếp chồng FAB bằng thanh ứng dụng dưới cùng:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to false
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

Hình 3. Scaffold sử dụng khe floatingActionButton và khe bottomBar. Thông số isFloatingActionButtonDocked được đặt thành false (trên cùng) và true (dưới cùng).

BottomAppBar hỗ trợ các vết cắt FAB có thông số cutoutShape, chấp nhận mọi Shape. Việc cung cấp cùng một Shape được thành phần đế sử dụng là một việc đáng thực hiện. Ví dụ: FloatingActionButton sử dụng MaterialTheme.shapes.small với kích thước góc là 50% làm giá trị mặc định cho thông số shape:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar(
            // Defaults to null, that is, No cutout
            cutoutShape = MaterialTheme.shapes.small.copy(
                CornerSize(percent = 50)
            )
        ) {
            /* Bottom app bar content */
        }
    }
) {
  // Screen content
}

Hình 4. ScaffoldBottomAppBar và một FloatingActionButton được gắn vào. BottomAppBar có một cutoutShape tuỳ chỉnh trùng khớp với Shape được FloatingActionButton sử dụng.

Các thanh thông báo nhanh

Scaffold cung cấp một phương tiện để hiển thị thanh thông báo nhanh.

Tính năng này được cung cấp thông qua ScaffoldState, bao gồm thuộc tính SnackbarHostState. Bạn có thể sử dụng rememberScaffoldState để tạo một phiên bản của ScaffoldState. Phiên bản này sẽ được chuyển tới Scaffold bằng thông số scaffoldState. SnackbarHostState cung cấp quyền truy cập vào hàm showSnackbar. Hàm tạm ngưng cần phải có CoroutineScope — ví dụ: sử dụng rememberCoroutineScope — và có thể được gọi lệnh để phản hồi sự kiện giao diện người dùng nhằm hiển thị Snackbar trong Scaffold.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    scaffoldState.snackbarHostState
                        .showSnackbar("Snackbar")
                }
            }
        )
    }
) {
    // Screen content
}

Bạn có thể cung cấp một hành động tuỳ chọn và điều chỉnh thời lượng của Snackbar. Hàm snackbarHostState.showSnackbar chấp nhận thông số actionLabelduration bổ sung, và trả về một SnackbarResult.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    val result = scaffoldState.snackbarHostState
                        .showSnackbar(
                            message = "Snackbar",
                            actionLabel = "Action",
                            // Defaults to SnackbarDuration.Short
                            duration = SnackbarDuration.Indefinite
                        )
                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }
        )
    }
) {
    // Screen content
}

Bạn có thể cung cấp Snackbar tuỳ chỉnh với thông số snackbarHost. Hãy xem SnackbarHost API reference docs để biết thêm thông tin.

Ngăn chứa

Scaffold cung cấp một khe cho ngăn điều hướng mẫu. Trang tính có thể kéo và bố cục của thành phần kết hợp được xử lý nội bộ.

Bạn có thể sử dụng khe drawerContent, trong đó sử dụng ColumnScope để sắp xếp bố cục các thành phần kết hợp nội dung ngăn chứa trong một cột:

Scaffold(
    drawerContent = {
        Text("Drawer title", modifier = Modifier.padding(16.dp))
        Divider()
        // Drawer items
    }
) {
    // Screen content
}

Scaffold chấp nhận một số thông số ngăn chứa bổ sung. Ví dụ: bạn có thể chuyển đổi để xem ngăn chứa có phản hồi với các lệnh kéo bằng thông số drawerGesturesEnabled hay không:

Scaffold(
    drawerContent = {
        // Drawer content
    },
    // Defaults to true
    drawerGesturesEnabled = false
) {
    // Screen content
}

Việc lập trình mở và đóng ngăn được thực hiện thông qua ScaffoldState, trong đó có một thuộc tính DrawerState sẽ được chuyển đếnScaffold với thông sốscaffoldState. DrawerState cung cấp quyền truy cập vào các hàm openclose, cũng như các thuộc tính liên quan đến trạng thái ngăn chứa hiện tại. Các hàm tạm ngưng này cần phải có CoroutineScope — ví dụ: sử dụng rememberCoroutineScope — và có thể được gọi lệnh để phản hồi các sự kiện giao diện người dùng.

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
        // Drawer content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Open or close drawer") },
            onClick = {
                scope.launch {
                    scaffoldState.drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

Nếu muốn triển khai ngăn điều hướng mẫu ở chế độ không có Scaffold, bạn có thể sử dụng thành phần kết hợp ModalDrawer. Thành phần này chấp nhận các thông số ngăn tương tự với Scaffold.

val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

Nếu muốn triển khai ngăn điều hướng ở dưới cùng, bạn có thể sử dụng thành phần kết hợp BottomDrawer:

val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
BottomDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

Bảng dưới cùng

Nếu muốn triển khai bảng dưới cùng tiêu chuẩn, bạn có thể sử dụng thành phần kết hợp BottomSheetScaffold. Thành phần này chấp nhận các thông số tương tự với Scaffold, chẳng hạn như topBar, floatingActionButtonsnackbarHost. Tính năng này bao gồm các thông số bổ sung cung cấp phương tiện để hiển thị các bảng dưới cùng.

Bạn có thể dùng khe sheetContent. Khe này sử dụng ColumnScope để sắp xếp bố cục thành phần kết hợp nội dung của bảng trong một cột:

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

BottomSheetScaffold chấp nhận một số thông số bảng tính bổ sung. Ví dụ: bạn có thể đặt chiều cao xem trước của bảng bằng thông số sheetPeekHeight. Bạn cũng có thể chuyển đổi xem ngăn chứa có phản hồi các lệnh kéo bằng thông số sheetGesturesEnabled.

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    },
    // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
    sheetPeekHeight = 128.dp,
    // Defaults to true
    sheetGesturesEnabled = false

) {
    // Screen content
}

Việc mở rộng và thu gọn bảng tính theo lập trình được thực hiện thông qua BottomSheetScaffoldState, trong đó có chứa thuộc tính BottomSheetState. Bạn có thể sử dụng rememberBottomSheetScaffoldState để tạo một phiên bản BottomSheetScaffoldState sẽ được chuyển cho BottomSheetScaffold bằng thông số scaffoldState. BottomSheetState cung cấp quyền truy cập vào các hàm expandcollapse cũng như các thuộc tính liên quan đến trạng thái hiện tại của bảng tính. Các hàm tạm ngưng này yêu cầu CoroutineScope — ví dụ: sử dụng rememberCoroutineScope — và có thể được gọi lệnh để phản hồi các sự kiện giao diện người dùng.

val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
    scaffoldState = scaffoldState,
    sheetContent = {
        // Sheet content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Expand or collapse sheet") },
            onClick = {
                scope.launch {
                    scaffoldState.bottomSheetState.apply {
                        if (isCollapsed) expand() else collapse()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

Nếu muốn triển khai bảng tính dưới cùng mẫu, bạn có thể sử dụng thành phần kết hợp ModalBottomSheetLayout:

val sheetState = rememberModalBottomSheetState(
    ModalBottomSheetValue.Hidden
)
ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

Phông nền

Nếu muốn triển khai phông nền, bạn có thể sử dụng thành phần kết hợp BackdropScaffold.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)

BackdropScaffold chấp nhận một số thông số phông nền bổ sung. Ví dụ: bạn có thể cài đặt chiều cao xem trước của lớp sau và chiều cao tối thiểu không hoạt động của lớp trước bằng các thông số peekHeightheaderHeight. Bạn cũng có thể bật/tắt chế độ phông nền xem có phản hồi các lệnh kéo bằng thông số gesturesEnabled hay không.

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    },
    // Defaults to BackdropScaffoldDefaults.PeekHeight
    peekHeight = 40.dp,
    // Defaults to BackdropScaffoldDefaults.HeaderHeight
    headerHeight = 60.dp,
    // Defaults to true
    gesturesEnabled = false
)

Việc công khai và ẩn phông nền có lập trình được thực hiện thông qua BackdropScaffoldState. Bạn có thể sử dụng rememberBackdropScaffoldState để tạo một phiên bản BackdropScaffoldState. Phiên bản này nên được chuyển cho BackdropScaffold bằng thông số scaffoldState. BackdropScaffoldState cung cấp quyền truy cập vào revealconceal, cũng như các thuộc tính liên quan đến trạng thái phông nền hiện tại. Các hàm tạm ngưng này yêu cầu CoroutineScope — ví dụ: sử dụng rememberCoroutineScope — và có thể được gọi lệnh để phản hồi các sự kiện giao diện người dùng.

val scaffoldState = rememberBackdropScaffoldState(
    BackdropValue.Concealed
)
val scope = rememberCoroutineScope()
BackdropScaffold(
    scaffoldState = scaffoldState,
    appBar = {
        TopAppBar(
            title = { Text("Backdrop") },
            navigationIcon = {
                if (scaffoldState.isConcealed) {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.reveal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Menu"
                        )
                    }
                } else {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.conceal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = "Close"
                        )
                    }
                }
            },
            elevation = 0.dp,
            backgroundColor = Color.Transparent
        )
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)