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 hiển thị nhất có thể (cho dù là nhiều màn hình thiết bị hay nhiều cửa sổ ứng dụng ở chế độ nhiều cửa sổ), 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 hiển thị, 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ị gập, thiết bị ChromeOS, hướng dọc và ngang cũng như các cấu hình hiển thị có thể đổi kích thước như chế độ chia đôi màn hình và cửa sổ kiểu máy tính.

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

Các 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 màn hình có sẵn cho ứng dụng. Đối với các 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 có sẵn cho giao diện người dùng.

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ể chia màn hình với một ứng dụng khác. Ở chế độ cửa sổ kiểu máy tính hoặc trên ChromeOS, một ứng dụng có thể nằm trong một cửa sổ có thể thay đổi kích thước. Thậm chí, có thể có nhiều màn hình thực, chẳng hạn như 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 vật lý không liên quan đến việc quyết định cách hiển thị nội dung.

Thay vào đó, hãy đưa ra 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ủa bạn, được mô tả bằng các chỉ số 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 màn hình có sẵn 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 đã xác định được các chỉ số của 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 các 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 sự linh hoạt nhằm tối ưu hoá ứng dụng cho hầu hết các kích thước hiển thị.

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 chúng 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(supportLargeAndXLargeWidth = true).windowSizeClass
) {
    // Decide whether to show the top app bar based on window size class.
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)

    // MyScreen logic is based on the showTopAppBar boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

Phương thức phân lớp này 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 chúng 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ư 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 sẽ 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ể tái sử dụng 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 hiển thị 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 danh sách-chi tiết, vốn có thể hiển thị một ngăn hoặc hai ngăn 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 điển hình – 1 là vùng danh sách; 2 là vùng 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 chuyển xuống 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 màn hình có sẵn thì sao? Ví dụ: một thẻ hiển thị thêm thông tin chi tiết nếu không gian cho phép. Bạn muốn thực hiện một số logic dựa trên một số kích thước hiển thị 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à mô tả ngắn.

Tránh cố gắng sử dụng kích thước màn hình thực tế của thiết bị. Điều này sẽ không chính xác đối với nhiều loại 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 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ư 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 màn hình 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 mà thành phần kết hợp thực sự được cấp để hiển thị chính nó. Bạn có 2 lựa 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 được hiển thị, hãy sử dụng bộ sưu tập công cụ sửa đổi hoặc bố cục tuỳ chỉnh để thích ứng hoá bố cục. Điều này có thể đơn giản như việc có một phần tử con lấp đầy toàn bộ không gian có sẵn hoặc bố trí các phần tử con bằng nhiều cột nếu có đủ không gian.

  • Nếu bạn muốn thay đổi nội dung hiển thị, hãy sử dụng BoxWithConstraints làm giải pháp thay thế mạnh mẽ 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 màn hình 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(/* ... */)
            }
        }
    }
}

Cung cấp tất cả dữ liệu 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ả như là tác dụng phụ của kích thước hiển thị 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 hiển thị, 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 chuyển 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ó description, bất kể chiều rộng có sẵn nào.

Việc luôn chuyể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 màn hình (đ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 của ứ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 của ứ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 bị 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 tối ưu cho người dùng 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 màn hình có sẵn
  • Reply 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