Hỗ trợ nhiều kích thước màn hình

Nhờ khả năng hỗ trợ nhiều kích thước màn hình, nhiều loại thiết bị và nhiều người dùng nhất có thể truy cập vào ứng dụng của bạn.

Để hỗ trợ nhiều kích thước màn hình nhất có thể, hãy thiết kế bố cục ứng dụng của bạn sao cho đáp ứng và thích ứng. Bố cục thích ứng cung cấp trải nghiệm người dùng được tối ưu hoá bất kể kích thước màn hình, cho phép ứng dụng của bạn thích ứng với điện thoại, máy tính bảng, thiết bị có thể gập lại, thiết bị ChromeOS, hướng dọc và ngang cũng như các cấu hình có thể đổi kích thước như chế độ nhiều cửa sổ.

Bố cục thích ứng thay đổi dựa trên không gian hiển thị có sẵn. Thay đổi đa dạng, từ các điều chỉnh nhỏ về bố cục lấp đầy không gian (thiết kế thích ứng) cho đến thay thế hoàn toàn một bố cục bằng một bố cục khác để ứng dụng có thể phù hợp nhất với nhiều kích thước hiển thị (thiết kế thích ứng).

Là một bộ công cụ giao diện người dùng mang tính khai báo, Jetpack Compose là lựa chọn lý tưởng để thiết kế và triển khai các bố cục thay đổi linh động để hiển thị nội dung theo cách khác nhau trên nhiều kích thước màn hình.

Thực hiện các thay đổi lớn về bố cục để thành phần kết hợp cấp màn hình trở nên rõ ràng

Khi sử dụng Compose để tạo bố cục cho toàn bộ ứng dụng, các thành phần kết hợp cấp ứng dụng và cấp màn hình sẽ chiếm toàn bộ không gian hiển thị dành cho ứng dụng. Ở cấp độ này trong thiết kế, bạn có thể thay đổi bố cục tổng thể của màn hình để tận dụng những màn hình lớn hơn.

Tránh sử dụng giá trị vật lý của phần cứng để đưa ra quyết định về bố cục. Bạn có thể muốn đưa ra quyết định dựa trên một giá trị hữu hình cố định (Thiết bị có phải là máy tính bảng không? Màn hình thực có tỷ lệ khung hình nhất định không?), nhưng câu trả lời cho những câu hỏi này có thể không hữu ích trong việc xác định không gian hoạt động của giao diện người dùng.

Sơ đồ cho thấy một số kiểu dáng thiết bị, bao gồm cả điện thoại, thiết bị có thể gập lại, máy tính bảng và máy tính xách tay.
Hình 1. Các kiểu dáng điện thoại, thiết bị gập, máy tính bảng và máy tính xách tay

Trên máy tính bảng, một ứng dụng có thể chạy ở chế độ nhiều cửa sổ khi chia đôi màn hình với một ứng dụng khác. Trên ChromeOS, một ứng dụng nằm trong một cửa sổ có thể đổi kích thước. Thậm chí có thể có nhiều hơn một màn hình vật lý, chẳng hạn như với thiết bị gập. Trong những trường hợp như vậy, kích thước màn hình thực tế không liên quan đến việc quyết định nội dung hiển thị.

Thay vào đó, bạn nên quyết định dựa trên phần màn hình thực tế được phân bổ cho ứng dụng, chẳng hạn như các chỉ số về cửa sổ hiện tại do thư viện JetPack WindowManager cung cấp. Để xem cách sử dụng WindowManager trong ứng dụng Compose, hãy xem mẫu JetNews.

Phương pháp này giúp ứng dụng của bạn linh hoạt hơn vì ứng dụng sẽ hoạt động tốt trong tất cả trường hợp nêu trên. Việc làm cho bố cục thích ứng với không gian màn hình cũng giúp giảm khối lượng xử lý đặc biệt để hỗ trợ các nền tảng như ChromeOS cũng như các kiểu dáng như máy tính bảng và thiết bị có thể gập lại.

Sau khi quan sát không gian liên quan hiện có cho ứng dụng, bạn nên chuyển đổi kích thước thô thành lớp kích thước có ý nghĩa, như mô tả trong phần Các lớp kích thước cửa sổ. Thông tin này sẽ nhóm các kích thước thành các nhóm kích thước tiêu chuẩn. Đây là các điểm ngắt được thiết kế để cân bằng giữa sự đơn giản và tính linh hoạt nhằm tối ưu hoá ứng dụng trong hầu hết các trường hợp riêng biệt. Các lớp kích thước này tham chiếu đến cửa sổ tổng thể của ứng dụng. Vì vậy, hãy sử dụng các lớp này khi quyết định bố cục ảnh hưởng đến bố cục màn hình tổng thể của bạn. Bạn có thể chuyển các lớp kích thước này xuống dưới dạng trạng thái hoặc thực hiện thêm logic để tạo trạng thái dẫn xuất nhằm chuyển xuống các thành phần kết hợp được lồng.

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Phương pháp phân lớp này giới hạn logic kích thước màn hình ở một vị trí duy nhất, thay vì phân tán chúng trên ứng dụng của bạn ở nhiều vị trí cần được đồng bộ hoá. Vị trí duy nhất này tạo ra trạng thái có thể được chuyển xuống một cách rõ ràng cho các thành phần kết hợp khác giống như cách bạn làm với bất kỳ trạng thái nào khác của ứng dụng. Việc chuyển trạng thái một cách rõ ràng sẽ đơn giản hoá các thành phần kết hợp riêng lẻ, vì chúng sẽ chỉ là các hàm có khả năng kết hợp thông thường với lớp kích thước hoặc cấu hình được chỉ định cùng với dữ liệu khác.

Có thể sử dụng lại các thành phần kết hợp được lồng linh hoạt

Các thành phần kết hợp dễ tái sử dụng hơn khi chúng được đặt trong nhiều vị trí khác nhau. Giả định một thành phần kết hợp sẽ luôn được đặt ở một vị trí nhất định với kích thước cụ thể, thì sẽ khó sử dụng lại được ở nơi khác trong một vị trí khác hoặc với những không gian còn trống khác. Điều này cũng có nghĩa là các thành phần kết hợp riêng lẻ, có thể sử dụng lại nên tránh ngầm phụ thuộc vào thông tin kích thước "chung".

Hãy xem xét ví dụ sau: Hãy tưởng tượng một thành phần kết hợp được lồng triển khai một bố cục chi tiết danh sách. Bố cục này có thể hiển thị một ngăn hoặc hai ngăn cạnh nhau.

Ảnh chụp màn hình một ứng dụng hiển thị hai ngăn cạnh nhau.
Hình 2. Ảnh chụp màn hình ứng dụng cho thấy bố cục danh sách-chi tiết điển hình: 1 là vùng danh sách; 2 là vùng chi tiết.

Chúng ta muốn quyết định này trở thành một phần của bố cục tổng thể cho ứng dụng, nên chúng ta chuyển quyết định này xuống từ thành phần kết hợp cấp màn hình như đã thấy ở trên:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

Điều gì sẽ xảy ra nếu chúng ta muốn một thành phần kết hợp độc lập thay đổi bố cục của nó dựa trên không gian có sẵn? Ví dụ: một thẻ muốn hiển thị thêm thông tin chi tiết nếu không gian cho phép. Chúng ta muốn thực hiện một số logic dựa trên một số kích thước có sẵn, nhưng cụ thể là kích thước nào?

Ví dụ về 2 thẻ khác nhau.
Hình 3. Thẻ hẹp chỉ cho thấy biểu tượng và tiêu đề, còn thẻ rộng hơn cho thấy biểu tượng, tiêu đề và nội dung mô tả ngắn.

Như đã thấy ở trên, chúng ta nên tránh sử dụng kích thước màn hình thực tế của thiết bị. Giá trị này sẽ không chính xác cho nhiều màn hình và cũng sẽ không chính xác nếu ứng dụng không ở chế độ toàn màn hình.

Vì thành phần kết hợp này không phải là thành phần kết hợp cấp màn hình, nên chúng ta cũng không nên sử dụng trực tiếp các chỉ số cửa sổ hiện tại để tối đa hoá khả năng sử dụng lại. Nếu thành phần này được đặt với khoảng đệm (chẳng hạn như phần lồng ghép) hoặc nếu có các thành phần như thanh điều hướng hoặc thanh ứng dụng, thì khoảng không gian còn trống cho thành phần kết hợp này có thể khác đáng kể so với tổng thể không gian còn trống cho ứng dụng.

Do đó, chúng ta nên sử dụng chiều rộng không gian được cấp cho thành phần kết hợp để hiển thị. Chúng ta có hai tuỳ chọn để lấy chiều rộng đó:

Nếu muốn thay đổi vị trí hoặc cách hiển thị nội dung, bạn có thể sử dụng bộ sưu tập đối tượng sửa đổi hoặc bố cục tuỳ chỉnh để bố cục thích ứng. Điều này có thể đơn giản như việc lấp đầy toàn bộ không gian còn trống bằng các tập con, hoặc sắp xếp chúng theo nhiều cột nếu có đủ chỗ.

Nếu muốn thay đổi nội dung hiển thị, bạn có thể sử dụng BoxWithConstraints như một giải pháp thay thế hiệu quả hơn. Thành phần kết hợp này cung cấp các giới hạn về đo lường mà bạn có thể sử dụng để gọi nhiều thành phần kết hợp dựa trên không gian hiện có. Tuy nhiên, điều này đi kèm một số chi phí, vì BoxWithConstraints trì hoãn quá trình kết hợp cho đến giai đoạn Bố cục, khi những hạn chế này đã được biết, khiến bạn phải thực hiện nhiều thao tác hơn trong bố cục.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

Đảm bảo tất cả dữ liệu đều có sẵn cho nhiều kích thước

Khi tận dụng không gian màn hình bổ sung trên một màn hình lớn, bạn có thể hiển thị nhiều nội dung tới khách hàng hơn so với một màn hình nhỏ. Khi triển khai một thành phần kết hợp tương ứng với hành vi này, có thể muốn tải dữ liệu như là hiệu ứng phụ của kích thước hiện tại.

Tuy nhiên, điều này đi ngược lại nguyên tắc về luồng dữ liệu một chiều, trong đó dữ liệu có thể được chuyển lên trên và cung cấp cho các thành phần kết hợp để hiển thị một cách phù hợp. Bạn nên cung cấp đủ dữ liệu cho thành phần kết hợp để thành phần kết hợp luôn có nội dung cần hiển thị trên mọi kích thước, ngay cả khi một số phần dữ liệu có thể không phải lúc nào cũng được sử dụng.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Dựa trên ví dụ về Card, hãy lưu ý rằng chúng ta luôn chuyển description cho Card. Mặc dù description chỉ được sử dụng khi chiều rộng cho phép hiển thị, nhưng Card luôn yêu cầu có nó, bất kể chiều rộng có sẵn nào.

Việc luôn truyền dữ liệu giúp bố cục thích ứng trở nên đơn giản hơn bằng cách làm cho bố cục ít trạng thái hơn và tránh kích hoạt các hiệu ứng phụ khi chuyển đổi giữa các kích thước (điều này có thể xảy ra do thay đổi kích thước cửa sổ, thay đổi hướng hoặc gập và mở thiết bị).

Nguyên tắc này cũng cho phép duy trì trạng thái trên các thay đổi của bố cục. Bằng cách chuyển những thông tin có thể không được dùng ở mọi kích thước lên trên, chúng ta có thể duy trì trạng thái của người dùng khi kích thước bố cục thay đổi. Ví dụ: chúng ta có thể chuyển một cờ Boolean showMore lên trên để trạng thái của người dùng được giữ nguyên khi việc đổi kích thước khiến bố cục chuyển đổi giữa ẩn và hiện nội dung mô tả:

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

Tìm hiểu thêm

Để tìm hiểu thêm về bố cục tuỳ chỉnh trong Compose, hãy tham khảo thêm các tài nguyên sau.

Ứng dụng mẫu

  • Bố cục chuẩn cho màn hình lớn là kho lưu trữ các mẫu thiết kế đã được kiểm chứng, mang lại trải nghiệm tối ưu cho người dùng trên các thiết bị màn hình lớn
  • JetNews cho biết cách thiết kế một ứng dụng điều chỉnh giao diện người dùng để tận dụng không gian có sẵn
  • Trả lời là một mẫu thích ứng để hỗ trợ thiết bị di động, máy tính bảng và thiết bị có thể gập lại
  • Now in Android là một ứng dụng dùng bố cục thích ứng để hỗ trợ nhiều kích thước màn hình

Video