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

Để 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 thích ứng và thích ứng. Bố cục thích ứng giúp tối ưu hoá người dùng bất kể kích thước màn hình, việc cho phép ứng dụng của bạn phù hợp 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, và các cấu hình có thể đổi kích thước, chẳng hạn như nhiều cửa sổ chế độ xem.

Bố cục thích ứng thay đổi dựa trên không gian hiển thị có sẵn. Thay đổi bao gồm từ những điều chỉnh nhỏ về bố cục nhằm lấp đầy không gian (thiết kế đáp ứ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ủa bạn có thể phù hợp nhất kích thước hiển thị khác nhau (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à công cụ lý tưởng để thiết kế và triển khai các bố cục tự động thay đổi để 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ị cuốn hút vào việc đư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 là những câu hỏi này có thể không hữu ích cho việc xác định không gian hoạt động của giao diện người dùng với.

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à ứng dụng 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 có thể ở trong 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 tất cả các 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, 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. Làm cho bố cục thích ứng với không gian màn hình đối với họ cũng giúp giảm bớt một yêu cầu hỗ trợ đặc biệt các nền tảng như ChromeOS và kiểu dáng thiết bị như máy tính bảng và thiết bị có thể gập lại.

Khi bạn quan sát được không gian liên quan có sẵn cho ứng dụng của mình, điều này rất hữu ích để chuyển đổi kích thước thô thành lớp kích thước có ý nghĩa, như được mô tả trong Window các lớp kích thước. Chiến dịch này nhóm kích thước vào nhóm kích thước chuẩn, là những điểm ngắt được thiết kế để tạo sự 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 sao cho độc đáo nhất trường hợp. 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 để biết 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 bạn có thể thực hiện logic bổ sung để tạo trạng thái dẫn xuất để chuyển đến 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ào đó phân tán nó trên ứng dụng của bạn ở nhiều nơi 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 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 nó sẽ luôn được đặt trong một có kích thước cụ thể, thì sẽ khó sử dụng lại vị trí đó ở nơi khác trong một vị trí khác hoặc với số lượng không gian sẵn có khác. Việc 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 về "toàn cầu" thông tin về kích thước.

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 giúp triển khai một danh sách-chi tiết bố cục, có thể hiển thị một ngăn hoặc 2 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à 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(/* ... */)
    }
}

Giả sử chúng ta muốn một thành phần kết hợp thay đổi độc lập bố cục của nó dựa trên dung lượng còn trống không? 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 thực màn hình. Giá trị 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 thức nội dung hiển thị, bạn có thể sử dụng tập hợp đối tượng sửa đổi hoặc bố cục tuỳ chỉnh để làm cho 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 tính năng đo lường quy tắc ràng buộc mà bạn có thể dùng để gọi các thành phần kết hợp khác nhau dựa trên không gian sẵn có. Tuy nhiên, việc này cũng đi đôi với chi phí, vì BoxWithConstraints trì hoãn 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, gây ra 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 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. Đủ rồi dữ liệu phải được cung cấp cho thành phần kết hợp để thành phần kết hợp luôn có hình ảnh 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 luôn đượ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 đơn giản hơn bằng cách làm cho chúng ít trạng thái hơn, và tránh gây ra tác dụng phụ khi chuyển đổi giữa các kích thước (có thể xảy ra do thay đổi kích thước cửa sổ, thay đổi hướng hoặc hoạt động 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 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 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 bị thay đổi giữa việc ẩn và hiển thị 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 người dùng tối ưu trải nghiệm của bạn trên thiết bị có màn hình lớn
  • JetNews trình bày cách thiết kế một ứng dụng có khả nă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ẫ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