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

Việc hỗ trợ nhiều kích thước màn hình giúp nhiều thiết bị và số lượng 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 đáp ứng/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. Các thay đổi có phạm vi từ những điều chỉnh bố cục nhỏ để lấp đầy không gian (thiết kế thích ứng) đế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ủa bạn 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 có khả năng thay đổi linh động để hiển thị nội dung theo cách khác nhau trên nhiều kích thước hiển thị.

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. Có thể bạn sẽ 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 tế có tỷ lệ khung hình nhất định không?), nhưng câu trả lời cho các 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ị có thể gập lại, 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ổ, tức là có thể đang 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 bớt 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.

Khi quan sát không gian thích hợp có sẵn 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 Lớp kích thước cửa sổ. Điều này nhóm kích thước thành các nhóm kích thước chuẩn. Đây là các điểm ngắt được thiết kế để cân bằng giữa tính đơ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 đề cập đế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 cho các quyết định về 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 có thể thực hiện logic bổ sung để tạo trạng thái phát sinh để truyền xuống các thành phần kết hợp lồng nhau.

@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 nó 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 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 ứng dụng nào khác. 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. Nếu một thành phần kết hợp giả định rằng 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ó hơn khi sử dụng lại thành phần đó ở nơi khác trong một vị trí khác hoặc với 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 "toàn cầu".

Hãy xem xét ví dụ sau: Hãy tưởng tượng một thành phần kết hợp lồng nhau triển khai bố cục danh sách-chi tiết, 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ị 2 ngăn cạnh nhau.
Hình 2. Ảnh chụp màn hình một ứng dụng cho thấy bố cục danh sách-chi tiết thông thường – 1 là khu vực danh sách; 2 là khu vực 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(/* ... */)
    }
}

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 dựa trên không gian có sẵn thì sao? 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ề hai thẻ khác nhau.
Hình 3. Thẻ hẹp chỉ hiển thị biểu tượng và tiêu đề, còn thẻ rộng hơn hiển thị 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ị. Chỉ số này sẽ không chính xác cho nhiều màn hình và cũng 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 nội dung hiển thị, 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 đ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, việc này cũng đi kèm với chi phí, vì BoxWithConstraints trì hoãn quá trình kết hợp cho đến giai đoạn Bố cục, khi đã biết các điều kiện ràng buộc này, khiến cần 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 thích hợp. Bạn phải 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 chúng ít trạng thái hơn và tránh kích hoạt hiệu ứng phụ khi chuyển đổi giữa các kích thước (có thể xảy ra do đổ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 lên trên những thông tin có thể không được sử dụng ở mọi kích thước, 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ể nâng 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 qua lại giữa việc ẩn và hiện phần 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 người dùng tối ưu trên các thiết bị màn hình lớn
  • JetNews trình bày 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à ứng dụng dùng bố cục thích ứng để hỗ trợ nhiều kích thước màn hình

Video