Bố cục luồng trong Compose

FlowRowFlowColumn là các thành phần kết hợp tương tự như RowColumn, nhưng khác ở chỗ các mục chuyển vào dòng tiếp theo khi vùng chứa hết dung lượng. Thao tác này sẽ tạo ra nhiều hàng hoặc cột. Bạn cũng có thể kiểm soát số lượng mục trong một hàng bằng cách đặt maxItemsInEachRow hoặc maxItemsInEachColumn. Thường thì bạn có thể sử dụng FlowRowFlowColumn để tạo bố cục thích ứng – nội dung sẽ không bị cắt bớt nếu các mục quá lớn so với một kích thước. Đồng thời, việc sử dụng kết hợp maxItemsInEach* với Modifier.weight(weight) có thể giúp tạo bố cục lấp đầy/mở rộng chiều rộng của hàng hoặc cột khi cần.

Ví dụ điển hình là đối với giao diện người dùng khối hoặc bộ lọc:

5 khối trong một FlowRow, hiển thị tràn sang dòng tiếp theo khi không còn dung lượng trống.
Hình 1. Ví dụ về FlowRow

Cách sử dụng cơ bản

Để sử dụng FlowRow hoặc FlowColumn, hãy tạo các thành phần kết hợp này và đặt các mục bên trong theo quy trình tiêu chuẩn:

@Composable
private fun FlowRowSimpleUsageExample() {
    FlowRow(modifier = Modifier.padding(8.dp)) {
        ChipItem("Price: High to Low")
        ChipItem("Avg rating: 4+")
        ChipItem("Free breakfast")
        ChipItem("Free cancellation")
        ChipItem("£50 pn")
    }
}

Đoạn mã này dẫn đến giao diện người dùng như minh hoạ ở trên, với các mục tự động chuyển đến hàng tiếp theo khi không còn không gian trong hàng đầu tiên.

Tính năng của bố cục luồng

Bố cục luồng có các tính năng và thuộc tính sau mà bạn có thể sử dụng để tạo nhiều bố cục trong ứng dụng của mình.

Sắp xếp trục chính: sắp xếp theo chiều ngang hoặc chiều dọc

Trục chính là trục nơi các mục được bố trí (ví dụ: trong FlowRow, các mục được sắp xếp theo chiều ngang). Tham số horizontalArrangement trong FlowRow kiểm soát cách phân phối không gian trống giữa các mục.

Bảng sau đây trình bày ví dụ về cách đặt horizontalArrangement trên các mục của FlowRow:

Đã đặt chế độ sắp xếp theo chiều ngang trên FlowRow

Kết quả

Arrangement.Start (Default)

Các mục được sắp xếp ở đầu

Arrangement.SpaceBetween

Sắp xếp các mục có khoảng trống ở giữa

Arrangement.Center

Các mục được sắp xếp ở giữa

Arrangement.End

Các mục được sắp xếp ở cuối

Arrangement.SpaceAround

Các mục được sắp xếp với không gian xung quanh

Arrangement.spacedBy(8.dp)

Các mục được đặt cách nhau theo một dp nhất định

Đối với FlowColumn, các tuỳ chọn tương tự cũng có sẵn cho verticalArrangement, với giá trị mặc định là Arrangement.Top.

Sắp xếp trục chéo

Trục chéo là trục ngược lại với trục chính. Ví dụ: trong FlowRow, đây là trục tung. Để thay đổi cách sắp xếp nội dung tổng thể bên trong vùng chứa theo trục chéo, hãy sử dụng verticalArrangement cho FlowRowhorizontalArrangement cho FlowColumn.

Đối với FlowRow, bảng sau đây trình bày các ví dụ về cách đặt verticalArrangement khác nhau cho các mục:

Đã thiết lập chế độ sắp xếp theo chiều dọc vào FlowRow

Kết quả

Arrangement.Top (Default)

Sắp xếp trên cùng vùng chứa

Arrangement.Bottom

Sắp xếp vùng chứa dưới cùng

Arrangement.Center

Sắp xếp trung tâm vùng chứa

Với FlowColumn, bạn có thể dùng các tuỳ chọn tương tự cho horizontalArrangement. Cách sắp xếp trục chéo mặc định là Arrangement.Start.

Căn chỉnh từng mục riêng lẻ

Bạn nên đặt từng mục riêng lẻ trong hàng bằng các cách căn chỉnh khác nhau. Thuộc tính này khác với verticalArrangementhorizontalArrangement vì nó căn chỉnh các mục trong dòng hiện tại. Bạn có thể áp dụng phương thức này bằng Modifier.align().

Ví dụ: khi các mục trong FlowRow có chiều cao khác nhau, hàng sẽ lấy chiều cao của mục lớn nhất và áp dụng Modifier.align(alignmentOption) cho các mục:

Đã căn chỉnh dọc trên FlowRow

Kết quả

Alignment.Top (Default)

Các mục được căn chỉnh lên trên cùng

Alignment.Bottom

Các mục được căn chỉnh xuống dưới cùng

Alignment.CenterVertically

Các mục được căn chỉnh vào chính giữa

FlowColumn có các lựa chọn tương tự. Cách căn chỉnh mặc định là Alignment.Start.

Tối đa số mục trong hàng hoặc cột

Tham số maxItemsInEachRow hoặc maxItemsInEachColumn xác định các mục tối đa trong trục chính để cho phép trong một dòng trước khi xuống dòng tiếp theo. Giá trị mặc định là Int.MAX_INT, cho phép nhiều mục nhất có thể, miễn là kích thước của các mục cho phép vừa với dòng.

Ví dụ: việc đặt maxItemsInEachRow sẽ buộc bố cục ban đầu chỉ có 3 mục:

Chưa đặt giới hạn tối đa

maxItemsInEachRow = 3

Chưa đặt giá trị tối đa trên hàng luồng Số mục tối đa được đặt trên hàng luồng

Các mục trong luồng tải từng phần

ContextualFlowRowContextualFlowColumn là phiên bản chuyên biệt của FlowRowFlowColumn, cho phép bạn tải từng phần nội dung của hàng hoặc cột luồng. Các mẫu này cũng cung cấp thông tin về vị trí của mục (chỉ mục, số hàng và kích thước có sẵn), chẳng hạn như mục có nằm ở hàng đầu tiên hay không. Điều này rất hữu ích cho các tập dữ liệu lớn và nếu bạn cần thông tin theo bối cảnh về một mục.

Tham số maxLines giới hạn số hàng hiển thị, còn tham số overflow chỉ định nội dung sẽ hiển thị khi đạt đến mục tràn mục, cho phép bạn chỉ định expandIndicator hoặc collapseIndicator tuỳ chỉnh.

Ví dụ: để hiển thị nút "+ (số mục còn lại)" hoặc "Hiển thị ít hơn":

val totalCount = 40
var maxLines by remember {
    mutableStateOf(2)
}

val moreOrCollapseIndicator = @Composable { scope: ContextualFlowRowOverflowScope ->
    val remainingItems = totalCount - scope.shownItemCount
    ChipItem(if (remainingItems == 0) "Less" else "+$remainingItems", onClick = {
        if (remainingItems == 0) {
            maxLines = 2
        } else {
            maxLines += 5
        }
    })
}
ContextualFlowRow(
    modifier = Modifier
        .safeDrawingPadding()
        .fillMaxWidth(1f)
        .padding(16.dp)
        .wrapContentHeight(align = Alignment.Top)
        .verticalScroll(rememberScrollState()),
    verticalArrangement = Arrangement.spacedBy(4.dp),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    maxLines = maxLines,
    overflow = ContextualFlowRowOverflow.expandOrCollapseIndicator(
        minRowsToShowCollapse = 4,
        expandIndicator = moreOrCollapseIndicator,
        collapseIndicator = moreOrCollapseIndicator
    ),
    itemCount = totalCount
) { index ->
    ChipItem("Item $index")
}

Ví dụ về các hàng luồng theo ngữ cảnh.
Hình 2. Ví dụ về ContextualFlowRow

Trọng lượng mặt hàng

Trọng lượng phát triển một mục dựa trên hệ số của mục đó và không gian có sẵn trên dòng chứa mục đó. Quan trọng là có sự khác biệt giữa FlowRowRow với cách sử dụng trọng số để tính chiều rộng của một mục. Đối với Rows, trọng số dựa trên tất cả các mục trong Row. Với FlowRow, trọng số dựa trên các mục trong dòng mà một mục được đặt vào, chứ không phải tất cả các mục trong vùng chứa FlowRow.

Ví dụ: nếu bạn có 4 mục đều nằm trên một hàng, mỗi mục có các trọng số 1f, 2f, 1f3f khác nhau, thì tổng trọng số là 7f. Không gian còn lại trong một hàng hoặc cột sẽ được chia cho 7f. Sau đó, chiều rộng của mỗi mục sẽ được tính theo công thức: weight * (remainingSpace / totalWeight).

Bạn có thể sử dụng kết hợp Modifier.weight và tối đa các mục với FlowRow hoặc FlowColumn để tạo một bố cục giống như lưới. Phương pháp này rất hữu ích khi tạo bố cục thích ứng có thể điều chỉnh theo kích thước của thiết bị.

Có một vài ví dụ về những gì bạn có thể đạt được bằng cách sử dụng các trọng số. Ví dụ: lưới nơi các mục có kích thước bằng nhau, như minh hoạ dưới đây:

Đã tạo lưới với hàng luồng
Hình 3. Sử dụng FlowRow để tạo lưới

Để tạo một lưới gồm các kích thước mục bằng nhau, bạn có thể làm như sau:

val rows = 3
val columns = 3
FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = rows
) {
    val itemModifier = Modifier
        .padding(4.dp)
        .height(80.dp)
        .weight(1f)
        .clip(RoundedCornerShape(8.dp))
        .background(MaterialColors.Blue200)
    repeat(rows * columns) {
        Spacer(modifier = itemModifier)
    }
}

Quan trọng là nếu bạn thêm một mục khác và lặp lại 10 lần thay vì 9, mục cuối cùng sẽ chiếm toàn bộ cột cuối cùng, vì tổng trọng số cho toàn bộ hàng là 1f:

Kích thước đầy đủ của mục cuối cùng ở chế độ lưới
Hình 4. Sử dụng FlowRow để tạo lưới trong đó mục cuối cùng chiếm toàn bộ chiều rộng

Bạn có thể kết hợp trọng số với Modifiers khác như Modifier.width(exactDpAmount), Modifier.aspectRatio(aspectRatio) hoặc Modifier.fillMaxWidth(fraction). Tất cả các đối tượng sửa đổi này đều hoạt động cùng nhau để cho phép định cỡ thích ứng các mục trong FlowRow (hoặc FlowColumn).

Bạn cũng có thể tạo một lưới xen kẽ gồm nhiều kích thước mục, trong đó 2 mục chiếm một nửa chiều rộng và một mục chiếm toàn bộ chiều rộng của cột tiếp theo:

Lưới xen kẽ với hàng luồng
Hình 5. FlowRow với kích thước thay thế của các hàng

Bạn có thể thực hiện việc này bằng đoạn mã sau:

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 2
) {
    val itemModifier = Modifier
        .padding(4.dp)
        .height(80.dp)
        .clip(RoundedCornerShape(8.dp))
        .background(Color.Blue)
    repeat(6) { item ->
        // if the item is the third item, don't use weight modifier, but rather fillMaxWidth
        if ((item + 1) % 3 == 0) {
            Spacer(modifier = itemModifier.fillMaxWidth())
        } else {
            Spacer(modifier = itemModifier.weight(0.5f))
        }
    }
}

Định cỡ phân số

Bằng cách sử dụng Modifier.fillMaxWidth(fraction), bạn có thể chỉ định kích thước của vùng chứa mà một mục sẽ chiếm. Điều này khác với cách hoạt động của Modifier.fillMaxWidth(fraction) khi áp dụng cho Row hoặc Column, vì các mục Row/Column chiếm một tỷ lệ phần trăm chiều rộng còn lại, thay vì chiều rộng của toàn bộ vùng chứa.

Ví dụ: Mã sau đây tạo ra các kết quả khác nhau khi sử dụng FlowRowRow:

FlowRow(
    modifier = Modifier.padding(4.dp),
    horizontalArrangement = Arrangement.spacedBy(4.dp),
    maxItemsInEachRow = 3
) {
    val itemModifier = Modifier
        .clip(RoundedCornerShape(8.dp))
    Box(modifier = itemModifier.height(200.dp).width(60.dp).background(Color.Red))
    Box(modifier = itemModifier.height(200.dp).fillMaxWidth(0.7f).background(Color.Blue))
    Box(modifier = itemModifier.height(200.dp).weight(1f).background(Color.Magenta))
}

FlowRow: Mục ở giữa bằng 0,7 phần chiều rộng của toàn bộ vùng chứa.

Chiều rộng thập phân với hàng luồng

Row: Mục ở giữa chiếm 0,7% chiều rộng còn lại của Row.

Chiều rộng thập phân có hàng

fillMaxColumnWidth()fillMaxRowHeight()

Việc áp dụng Modifier.fillMaxColumnWidth() hoặc Modifier.fillMaxRowHeight() cho một mục bên trong FlowColumn hoặc FlowRow đảm bảo rằng các mục trong cùng một cột hoặc hàng có cùng chiều rộng hoặc chiều cao với mục lớn nhất trong cột/hàng.

Ví dụ: ví dụ này sử dụng FlowColumn để hiển thị danh sách các món tráng miệng Android. Bạn có thể thấy sự khác biệt về chiều rộng của từng mục khi Modifier.fillMaxColumnWidth() được áp dụng cho các mục so với khi không áp dụng và khi gói các mục.

FlowColumn(
    Modifier
        .padding(20.dp)
        .fillMaxHeight()
        .fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
    maxItemsInEachColumn = 5,
) {
    repeat(listDesserts.size) {
        Box(
            Modifier
                .fillMaxColumnWidth()
                .border(1.dp, Color.DarkGray, RoundedCornerShape(8.dp))
                .padding(8.dp)
        ) {

            Text(
                text = listDesserts[it],
                fontSize = 18.sp,
                modifier = Modifier.padding(3.dp)
            )
        }
    }
}

Đã áp dụng Modifier.fillMaxColumnWidth() cho mỗi mục

fillMaxColumnWidth

Chưa đặt thay đổi chiều rộng (gói các mục)

Chưa đặt chiều rộng cột tối đa lấp đầy