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 ứng dụng của bạn tiếp cận được nhiều người dùng nhất và cho nhiều loại thiết bị nhất.

Để hỗ trợ nhiều kích thước màn hình nhất có thể (dù là màn hình thiết bị hay cửa sổ ứng dụng ở chế độ nhiều cửa sổ), hãy thiết kế bố cục ứng dụng sao cho thích ứng và đáp ứng. Bố cục thích ứng/đáp ứ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 màn hình có thể đổi kích thước như chế độ chia đôi màn hình và cửa sổ máy tính.

Bố cục thích ứng/đáp ứng thay đổi dựa trên không gian hiển thị có sẵn. Các thay đổi dao động từ những điều chỉnh nhỏ về bố cục để lấp đầy không gian (thiết kế thích ứng) đến việc 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ể thích ứng tốt nhất với nhiều kích thước màn hình (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 rất phù hợp để 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 các 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 nội dung trở nên rõ ràng

Thành phần kết hợp cấp ứng dụng và cấp nội dung chiếm toàn bộ không gian hiển thị có sẵn cho ứng dụng. Đối với những loại thành phần kết hợp này, bạn nên thay đổi bố cục tổng thể của ứng dụng trên màn hình lớn.

Tránh sử dụng giá trị phần cứng thực tế để đưa ra quyết định về bố cục. Có thể bạn sẽ muốn đưa ra các 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 các câu hỏi này có thể không hữu ích trong việc xác định không gian giao diện người dùng khả thi.

Hình 1. 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ổ, nghĩa là ứng dụng đó có thể đang chia đôi màn hình với một ứng dụng khác. Ở chế độ cửa sổ trên máy tính hoặc trên ChromeOS, một ứng dụng có thể ở trong một cửa sổ có thể đổi kích thước. Thậm chí có thể có nhiều màn hình vật lý, chẳng hạn như với thiết bị có thể gập lại. Trong tất cả những trường hợp này, kích thước màn hình thực tế không liên quan đến việc quyết định cách hiển thị nội dung.

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, được mô tả bằng các chỉ số về cửa sổ hiện tại do thư viện JetPack WindowManager cung cấp. Để biết ví dụ về cách sử dụng WindowManager trong ứng dụng Compose, hãy xem mẫu JetNews.

Việc làm cho bố cục thích ứng với không gian hiển thị cũng giúp giảm bớt khối lượng xử lý đặc biệt cần thiế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 bạn đã xác định các chỉ số về không gian có sẵn cho ứng dụng, hãy chuyển đổi kích thước thô thành lớp kích thước cửa sổ như mô tả trong phần Sử dụng lớp kích thước cửa sổ. Các lớp kích thước cửa sổ là các điểm ngắt được thiết kế để cân bằng sự đơn giản của logic ứng dụng với tính linh hoạt nhằm tối ưu hoá ứng dụng cho hầu hết kích thước màn hình. Các lớp kích thước cửa sổ 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 cho các quyết định bố cục ảnh hưởng đến bố cục tổng thể của ứng dụng. Bạn có thể chuyển các lớp kích thước cửa sổ xuống dưới dạng trạng thái hoặc thực hiện logic bổ sung để tạo trạng thái dẫn xuất nhằm chuyển xuống những thành phần kết hợp được lồng.

@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 giới hạn logic kích thước hiển thị ở một vị trí duy nhất thay vì phân tán logic đó trên ứng dụng ở những vị trí cần phải được đồng bộ hoá. Một vị trí duy nhất 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ì các thành phần kết hợp này lấy lớp kích thước cửa sổ hoặc cấu hình được chỉ định cùng với dữ liệu khác.

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

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 phải được đặt ở một vị trí cụ thể với kích thước cụ thể, thì thành phần kết hợp đó khó có thể được sử dụng lại trong các ngữ cảnh 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 màn hình chung.

Hãy tưởng tượng một thành phần kết hợp được lồng chạy một bố cục chi tiết danh sách, vốn có thể hiển thị một ô hoặc hai ô cạnh nhau:

Một ứng dụng hiển thị hai ngăn cạnh nhau.
Hình 2. Ứng dụng hiển thị 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.

Quyết định về danh sách-chi tiết phải là một phần của bố cục tổng thể cho ứng dụng, vì vậy, quyết định này được truyền xuống từ một thành phần kết hợp cấp nội dung:

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

Nếu bạn 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 hiển thị có sẵn, ví dụ: một thẻ hiển thị thêm thông tin chi tiết nếu không gian cho phép thì sao? Bạn muốn thực hiện một số logic dựa trên kích thước màn hình có sẵn, nhưng cụ thể là kích thước nào?

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.

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 loại 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 nội dung, nên đừng sử dụng trực tiếp các chỉ số cửa sổ hiện tại. Nếu thành phần này được đặt với khoảng đệm (chẳng hạn như với phần lồng ghép) hoặc nếu ứng dụng bao gồm các thành phần như thanh điều hướng hoặc thanh ứng dụng, thì khoảng không gian hiển thị 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.

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ị. Bạn có hai tuỳ chọn để lấy chiều rộng đó:

  • Nếu bạn muốn thay đổi vị trí hoặc cách thức nội dung hiển thị, hãy 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 phần tử con, hoặc sắp xếp các phần tử con theo nhiều cột nếu có đủ chỗ.

  • Nếu bạn muốn thay đổi nội dung hiển thị, hãy sử dụng BoxWithConstraints như một giải pháp thay thế hiệu quả hơn. BoxWithConstraints 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 thị có sẵn. Tuy nhiên, điều này cũng đi kèm một số phí tổn, do BoxWithConstraints trì hoãn sự kết hợp tới giai đoạn bố cục, khi những điểm hạn chế này được biết đến, làm tăng khối lượng công việc cần thực hiệ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 màn hình

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

Tuy nhiên, việc 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à đưa tới các thành phần kết hợp để hiển thị một cách thích hợp. Bạn cần cung cấp đủ dữ liệu cho các thành phần kết hợp để các cấu trúc đó luôn có đủ nội dung cho mọi kích thước màn hình, ngay cả khi một số phần nội dung 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 description luôn được truyền đến 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ó description, bất kể chiều rộng có sẵn nào.

Việc luôn truyền đủ nội dung giúp bố cục thích ứng trở nên đơn giản hơn bằng cách hiển thị bố cục í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 hiển thị (điều 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ở màn hình 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 các thông tin có thể không được sử dụng ở mọi kích thước màn hình, bạn có thể bảo toàn trạng thái ứng dụng khi kích thước bố cục thay đổi. Ví dụ: bạn có thể chuyển một cờ boolean showMore lên trên để trạng thái ứng dụng được giữ nguyên khi việc thay đổi kích thước màn hình khiến bố cục chuyển đổi qua lại giữa ẩn và hiện nội dung:

@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 thích ứng trong Compose, hãy xem các tài nguyên sau:

Ứng dụng mẫu

  • CanonicalLayouts là kho lưu trữ các mẫu thiết kế đã được chứng minh, mang lại trải nghiệm người dùng tối ưu trên 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 cho phù hợp để tận dụng không gian hiển thị 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 sử dụng bố cục thích ứng để hỗ trợ nhiều kích thước màn hình

Video