Cung cấp bố cục tiện ích linh hoạt

Thử cách dùng Compose
Jetpack Compose là bộ công cụ giao diện người dùng được đề xuất cho Android. Tìm hiểu cách tạo tiện ích bằng API kiểu Compose.

Trang này mô tả các điểm tinh chỉnh về kích thước tiện ích và tính linh hoạt cao hơn được giới thiệu trong Android 12 (cấp độ API 31). Bài viết này cũng nêu chi tiết cách xác định kích thước cho tiện ích.

Sử dụng các API cải tiến cho kích thước và bố cục tiện ích

Kể từ Android 12 (cấp độ API 31), bạn có thể cung cấp các thuộc tính kích thước tinh tế hơn và bố cục linh hoạt hơn bằng cách làm như sau, như mô tả trong các phần sau:

  1. Chỉ định các quy tắc ràng buộc khác về kích thước tiện ích.

  2. Cung cấp bố cục thích ứng hoặc bố cục chính xác.

Trong các phiên bản Android trước, bạn có thể lấy phạm vi kích thước của một tiện ích bằng cách sử dụng các thành phần bổ sung OPTION_APPWIDGET_MIN_WIDTH, OPTION_APPWIDGET_MIN_HEIGHT, OPTION_APPWIDGET_MAX_WIDTH, và OPTION_APPWIDGET_MAX_HEIGHT rồi ước tính kích thước của tiện ích, nhưng logic đó không hoạt động trong mọi trường hợp. Đối với các tiện ích nhắm đến Android 12 trở lên, bạn nên cung cấp bố cục thích ứng hoặc bố cục chính xác.

Chỉ định các quy tắc ràng buộc bổ sung về kích thước tiện ích

Android 12 bổ sung các API giúp bạn đảm bảo tiện ích của mình có kích thước đáng tin cậy hơn trên nhiều thiết bị có kích thước màn hình khác nhau.

Ngoài các thuộc tính minWidth, minHeight, minResizeWidth và minResizeHeight hiện có, hãy sử dụng các thuộc tính appwidget-provider mới sau đây:

  • targetCellWidth và targetCellHeight: xác định kích thước mục tiêu của tiện ích theo các ô lưới của trình chạy. Nếu được xác định, các thuộc tính này sẽ được sử dụng thay vì minWidth hoặc minHeight.

  • maxResizeWidth và maxResizeHeight: xác định kích thước tối đa mà trình chạy cho phép người dùng đổi kích thước tiện ích.

XML sau đây cho biết cách sử dụng các thuộc tính định cỡ.

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:targetCellWidth="3"
  android:targetCellHeight="2"
  android:maxResizeWidth="250dp"
  android:maxResizeHeight="110dp">
</appwidget-provider>

Cung cấp bố cục thích ứng

Nếu bố cục cần thay đổi tuỳ thuộc vào kích thước của tiện ích, bạn nên tạo một nhóm nhỏ các bố cục, mỗi bố cục hợp lệ cho một dải kích thước. Nếu không thể, bạn có thể cung cấp bố cục dựa trên kích thước chính xác của tiện ích trong thời gian chạy, như mô tả trên trang này.

Tính năng này giúp điều chỉnh tỷ lệ mượt mà hơn và cải thiện tình trạng tổng thể của hệ thống, vì hệ thống không phải đánh thức ứng dụng mỗi khi hiển thị tiện ích ở một kích thước khác.

Đoạn mã ví dụ sau đây cho thấy cách cung cấp danh sách bố cục.

override fun onUpdate(
    /* ... */
) {
    val smallView = RemoteViews(
        /* ... */
    )
    val tallView = RemoteViews(
        /* ... */
    )
    val wideView = RemoteViews(
        /* ... */
    )

    val viewMapping: Map<SizeF, RemoteViews> = mapOf(
        SizeF(150f, 100f) to smallView,
        SizeF(150f, 200f) to tallView,
        SizeF(215f, 100f) to wideView
    )
    val remoteViews = RemoteViews(viewMapping)

    appWidgetManager.updateAppWidget(id, remoteViews)
}

Giả sử tiện ích có các thuộc tính sau:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minResizeWidth="160dp"
    android:minResizeHeight="110dp"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="200dp">
</appwidget-provider>

Đoạn mã trước đó có nghĩa như sau:

  • smallView hỗ trợ từ 160 dp (minResizeWidth) × 110 dp (minResizeHeight) đến 160 dp × 199 dp (điểm cắt tiếp theo – 1 dp).
  • tallView hỗ trợ từ 160dp × 200dp đến 214dp (điểm cắt tiếp theo – 1) × 200dp.
  • wideView hỗ trợ từ 215 dp × 110 dp (minResizeHeight) đến 250 dp (maxResizeWidth) × 200 dp (maxResizeHeight).

Tiện ích của bạn phải hỗ trợ phạm vi kích thước từ minResizeWidth × minResizeHeight đến maxResizeWidth × maxResizeHeight. Trong phạm vi đó, bạn có thể quyết định điểm ngắt để chuyển đổi bố cục.

Ví dụ về bố cục thích ứng
Hình 1. Ví dụ về bố cục thích ứng.

Cung cấp bố cục chính xác

Nếu không thể sử dụng một nhóm nhỏ bố cục thích ứng, bạn có thể cung cấp các bố cục khác nhau phù hợp với kích thước mà tiện ích hiển thị. Thông thường, có hai kích thước cho điện thoại (chế độ dọc và chế độ ngang) và bốn kích thước cho thiết bị có thể gập lại.

Để triển khai giải pháp này, ứng dụng của bạn cần thực hiện các bước sau:

  1. Nạp chồng AppWidgetProvider.onAppWidgetOptionsChanged(), được gọi khi tập hợp kích thước thay đổi.

  2. Gọi AppWidgetManager.getAppWidgetOptions(), trả về một Bundle chứa các kích thước.

  3. Truy cập vào khoá AppWidgetManager.OPTION_APPWIDGET_SIZES từ Bundle.

Ví dụ về mã sau đây cho thấy cách cung cấp bố cục chính xác.

override fun onAppWidgetOptionsChanged(
    context: Context,
    appWidgetManager: AppWidgetManager,
    id: Int,
    newOptions: Bundle?
) {
    super.onAppWidgetOptionsChanged(context, appWidgetManager, id, newOptions)
    // Get the new sizes.
    val sizes = newOptions?.let {
        BundleCompat.getParcelableArrayList(
            it,
            AppWidgetManager.OPTION_APPWIDGET_SIZES,
            SizeF::class.java
        )
    }
    // Check that the list of sizes is provided by the launcher.
    if (sizes.isNullOrEmpty()) {
        return
    }
    // Map the sizes to the RemoteViews that you want.
    val remoteViews = RemoteViews(sizes.associateWith(::createRemoteViews))
    appWidgetManager.updateAppWidget(id, remoteViews)
}

// Create the RemoteViews for the given size.
private fun createRemoteViews(size: SizeF): RemoteViews {
}

Xác định kích thước cho tiện ích

Mỗi tiện ích phải xác định targetCellWidth và targetCellHeight cho các thiết bị chạy Android 12 trở lên, hoặc minWidth và minHeight cho tất cả phiên bản Android, cho biết lượng không gian tối thiểu mà tiện ích đó sử dụng theo mặc định. Tuy nhiên, khi người dùng thêm một tiện ích vào màn hình chính, tiện ích đó thường chiếm nhiều hơn chiều rộng và chiều cao tối thiểu mà bạn chỉ định.

Màn hình chính Android cung cấp cho người dùng một lưới các không gian có sẵn để họ có thể đặt các tiện ích và biểu tượng. Lưới này có thể khác nhau tuỳ theo thiết bị; ví dụ: nhiều thiết bị cầm tay có lưới 5x4 và máy tính bảng có thể có lưới lớn hơn. Khi được thêm, tiện ích sẽ được kéo giãn để chiếm số lượng ô tối thiểu (theo chiều ngang và chiều dọc) cần thiết để đáp ứng các quy tắc ràng buộc cho targetCellWidth và targetCellHeight trên thiết bị chạy Android 12 trở lên, hoặc các quy tắc ràng buộc minWidth và minHeight trên thiết bị chạy Android 11 (cấp độ API 30) trở xuống.

Kích thước ô và lề tự động sẽ khác nhau tuỳ theo thiết bị. Hãy sử dụng bảng sau để ước tính kích thước tối thiểu của tiện ích trên một thiết bị cầm tay có lưới 5x4 thông thường, dựa trên số lượng ô lưới mà bạn muốn:

Số ô (chiều rộng x chiều cao) Kích thước hiện có ở chế độ chân dung (dp) Kích thước có sẵn ở chế độ ngang (dp)
1x1 57x102dp 127x51dp
2x1 130x102dp 269x51dp
3x1 203x102dp 412x51dp
4x1 276x102dp 554x51dp
5x1 349x102dp 697x51dp
5x2 349x220dp 697x117dp
5x3 349x337dp 697x184dp
5x4 349x455dp 697x250dp
... ... ...
n x m (73n - 16) x (118m - 16) (142n - 15) x (66m - 15)

Sử dụng kích thước ô ở chế độ chân dung để thông báo các giá trị bạn cung cấp cho các thuộc tính minWidth, minResizeWidth và maxResizeWidth. Tương tự, hãy sử dụng kích thước ô ở chế độ ngang để cung cấp thông tin cho các giá trị bạn cung cấp cho các thuộc tính minHeight, minResizeHeight và maxResizeHeight.

Lý do là vì chiều rộng ô thường nhỏ hơn ở chế độ dọc so với chế độ ngang và tương tự, chiều cao ô thường nhỏ hơn ở chế độ ngang so với chế độ dọc.

Ví dụ: nếu muốn chiều rộng của tiện ích có thể đổi kích thước xuống còn một ô trên Google Pixel 4, bạn cần đặt minResizeWidth thành tối đa 56 dp để đảm bảo giá trị của thuộc tính minResizeWidth nhỏ hơn 57 dp, vì một ô có chiều rộng ít nhất là 57 dp theo chiều dọc. Tương tự, nếu muốn chiều cao của tiện ích có thể đổi kích thước trong một ô trên cùng một thiết bị, bạn cần đặt minResizeHeight thành tối đa 50 dp để đảm bảo giá trị cho thuộc tính minResizeHeight nhỏ hơn 51 dp, vì một ô có chiều cao ít nhất là 51 dp ở chế độ ngang.

Mỗi tiện ích có thể đổi kích thước trong phạm vi kích thước giữa các thuộc tính minResizeWidth/minResizeHeight và maxResizeWidth/maxResizeHeight, tức là tiện ích cần thích ứng với mọi phạm vi kích thước giữa các thuộc tính đó.

Ví dụ: để đặt kích thước mặc định của tiện ích khi đặt, bạn có thể đặt các thuộc tính sau:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:targetCellWidth="3"
    android:targetCellHeight="2"
    android:minWidth="180dp"
    android:minHeight="110dp">
</appwidget-provider>

Điều này có nghĩa là kích thước mặc định của tiện ích là 3x2 ô, như được chỉ định bởi các thuộc tính targetCellWidth và targetCellHeight – hoặc 180×110 dp, như được chỉ định bởi minWidth và minHeight cho các thiết bị chạy Android 11 trở xuống. Trong trường hợp sau, kích thước theo ô có thể thay đổi tuỳ theo thiết bị.

Ngoài ra, để đặt phạm vi kích thước được hỗ trợ của tiện ích, bạn có thể đặt các thuộc tính sau:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minResizeWidth="180dp"
    android:minResizeHeight="110dp"
    android:maxResizeWidth="530dp"
    android:maxResizeHeight="450dp">
</appwidget-provider>

Như được chỉ định bởi các thuộc tính trước đó, chiều rộng của tiện ích có thể đổi kích thước từ 180 dp thành 530 dp và chiều cao của tiện ích có thể đổi kích thước từ 110 dp thành 450 dp. Sau đó, bạn có thể đổi kích thước tiện ích từ 3x2 thành 5x2 ô, miễn là đáp ứng các điều kiện sau:

val smallView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_small)
val mediumView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_medium)
val largeView = RemoteViews(context.packageName, R.layout.widget_weather_forecast_large)

val viewMapping: Map<SizeF, RemoteViews> = mapOf(
    SizeF(180f, 110f) to smallView,
    SizeF(270f, 110f) to mediumView,
    SizeF(270f, 280f) to largeView
)

appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))

Giả sử tiện ích sử dụng bố cục thích ứng được xác định trong các đoạn mã trước đó. Điều này có nghĩa là bố cục được chỉ định là R.layout.widget_weather_forecast_small được sử dụng từ 180dp (minResizeWidth) x 110dp (minResizeHeight) đến 269x279dp (điểm giới hạn tiếp theo – 1). Tương tự, R.layout.widget_weather_forecast_medium được dùng từ 270x110dp đến 270x279dp, và R.layout.widget_weather_forecast_large được dùng từ 270x280dp đến 530dp (maxResizeWidth) x 450dp (maxResizeHeight).

Khi người dùng đổi kích thước tiện ích, giao diện của tiện ích sẽ thay đổi để thích ứng với từng kích thước trong ô, như trong các ví dụ sau.

Ví dụ về tiện ích thời tiết ở kích thước lưới 3x2 nhỏ nhất. Giao diện người dùng cho thấy
            tên vị trí (Tokyo), nhiệt độ (14°) và biểu tượng cho biết
            thời tiết có mây vài nơi.
Hình 2. 3x2 R.layout.widget_weather_forecast_small.

Ví dụ về tiện ích thời tiết có kích thước &quot;trung bình&quot; 4x2. Việc đổi kích thước tiện ích theo cách này sẽ dựa trên tất cả giao diện người dùng của kích thước tiện ích trước đó, đồng thời thêm nhãn &quot;Nhiều mây&quot; và thông tin dự báo nhiệt độ từ 4 giờ chiều đến 7 giờ tối.
Hình 3. 4x2 R.layout.widget_weather_forecast_medium.

Ví dụ về tiện ích thời tiết có kích thước &quot;trung bình&quot; 5x2. Việc đổi kích thước tiện ích theo cách này sẽ tạo ra giao diện người dùng giống như kích thước trước đó, ngoại trừ việc tiện ích này được kéo dài thêm một ô để chiếm nhiều không gian ngang hơn.
Hình 4. 5x2 R.layout.widget_weather_forecast_medium.

Ví dụ về tiện ích thời tiết có kích thước &quot;lớn&quot; 5x3. Việc đổi kích thước tiện ích theo cách này sẽ dựa trên tất cả giao diện người dùng của các kích thước tiện ích trước đó, đồng thời thêm một chế độ xem bên trong tiện ích chứa thông tin dự báo thời tiết vào thứ Ba và thứ Tư. Biểu tượng cho biết thời tiết nắng hay mưa
            và nhiệt độ cao nhất cũng như thấp nhất mỗi ngày.
Hình 5. 5x3 R.layout.widget_weather_forecast_large.

Ví dụ về tiện ích thời tiết ở kích thước &quot;lớn&quot; 5x4. Việc đổi kích thước tiện ích theo cách này sẽ dựa trên tất cả giao diện người dùng của các kích thước tiện ích trước đó, đồng thời thêm thứ Năm và thứ Sáu (cùng các biểu tượng tương ứng cho biết loại thời tiết cũng như nhiệt độ cao và thấp cho mỗi ngày).
Hình 6. 5x4 R.layout.widget_weather_forecast_large.