Xây dựng giao diện người dùng bằng tính năng Xem nhanh

Trang này mô tả cách xử lý kích thước và cung cấp bố cục linh hoạt và thích ứng bằng Glance, sử dụng các thành phần Glance hiện có.

Sử dụng Box, ColumnRow

Glance có 3 bố cục kết hợp chính:

  • Box: Đặt các phần tử lên trên một phần tử khác. Thao tác này sẽ chuyển thành RelativeLayout.

  • Column: Sắp xếp các phần tử theo chiều dọc. Thao tác này sẽ chuyển thành LinearLayout có hướng dọc.

  • Row: Sắp xếp các phần tử lần lượt theo trục hoành. Thao tác này chuyển thành LinearLayout có hướng ngang.

Glance hỗ trợ các đối tượng Scaffold. Đặt các thành phần kết hợp Column, RowBox trong một đối tượng Scaffold nhất định.

Bố cục cột, hàng và hộp.
Hình 1. Ví dụ về bố cục có Cột, Hàng và Hộp.

Mỗi thành phần kết hợp này cho phép bạn xác định các căn chỉnh dọc và ngang của nội dung cũng như các ràng buộc về chiều rộng, chiều cao, trọng số hoặc khoảng đệm bằng cách sử dụng các đối tượng sửa đổi. Ngoài ra, mỗi thành phần con có thể xác định đối tượng sửa đổi của riêng mình để thay đổi khoảng trống và vị trí bên trong thành phần mẹ.

Ví dụ sau đây cho bạn thấy cách tạo một Row phân phối đều các phần tử con theo chiều ngang, như trong Hình 1:

Row(modifier = GlanceModifier.fillMaxWidth().padding(16.dp)) {
    val modifier = GlanceModifier.defaultWeight()
    Text("first", modifier)
    Text("second", modifier)
    Text("third", modifier)
}

Row sẽ lấp đầy chiều rộng tối đa có thể và vì mỗi phần tử con đều có cùng trọng số, nên chúng sẽ chia sẻ đều không gian có sẵn. Bạn có thể xác định các trọng số, kích thước, khoảng đệm hoặc căn chỉnh khác nhau để điều chỉnh bố cục cho phù hợp với nhu cầu của mình.

Sử dụng bố cục có thể cuộn

Một cách khác để cung cấp nội dung thích ứng là tạo nội dung có thể cuộn. Bạn có thể làm điều này bằng thành phần kết hợp LazyColumn. Thành phần kết hợp này cho phép bạn xác định một nhóm các mục sẽ được hiển thị bên trong một vùng chứa có thể cuộn trong tiện ích ứng dụng.

Các đoạn mã sau đây cho thấy nhiều cách xác định các mục bên trong LazyColumn.

Bạn có thể cung cấp số lượng mặt hàng:

// Remember to import Glance Composables
// import androidx.glance.appwidget.layout.LazyColumn

LazyColumn {
    items(10) { index: Int ->
        Text(
            text = "Item $index",
            modifier = GlanceModifier.fillMaxWidth()
        )
    }
}

Cung cấp từng mục riêng lẻ:

LazyColumn {
    item {
        Text("First Item")
    }
    item {
        Text("Second Item")
    }
}

Cung cấp danh sách hoặc mảng các mục:

LazyColumn {
    items(peopleNameList) { name ->
        Text(name)
    }
}

Bạn cũng có thể kết hợp các ví dụ trên:

LazyColumn {
    item {
        Text("Names:")
    }
    items(peopleNameList) { name ->
        Text(name)
    }

    // or in case you need the index:
    itemsIndexed(peopleNameList) { index, person ->
        Text("$person at index $index")
    }
}

Xin lưu ý rằng đoạn mã trước đó không chỉ định itemId. Việc chỉ định itemId giúp cải thiện hiệu suất và duy trì vị trí cuộn thông qua các bản cập nhật danh sách và appWidget từ Android 12 trở đi (ví dụ: khi thêm hoặc xoá các mục khỏi danh sách). Ví dụ sau đây cho thấy cách chỉ định một itemId:

items(items = peopleList, itemId = { person -> person.id.hashCode().toLong() }) { person ->
    Text(person.name)
}

Cuộn nhanh

Thao tác cuộn nhanh là một ảnh động cho phép nội dung có thể cuộn chuyển nhanh đến đầu vùng chứa tiện ích.

Video 1. Bên trái cho thấy một mục danh sách không khớp vào đúng vị trí khi cuộn, còn bên phải thì khớp vào đúng vị trí.


Để triển khai tính năng cuộn nhanh, hãy đảm bảo bạn đáp ứng các điều kiện sau:

  • Cập nhật phần phụ thuộc Glance lên phiên bản 1.3.0-alpha02 trở lên.
  • Đặt compileSdk thành 37 trở lên, vì tính năng cuộn nhanh được hỗ trợ trên các thiết bị chạy Android 17 trở lên.
  • Định cấu hình LazyColumn bằng VerticalScrollMode. Nếu thiết bị hỗ trợ tính năng cuộn nhanh, hãy sử dụng SnapScrollMatchHeight. Nếu không, hãy sử dụng Normal.

Nếu bạn đang sử dụng tính năng cuộn nhanh với hình ảnh, hãy xem Bố cục chuẩn hình ảnh tràn viền.

@Composable
fun SnapScrollLayout() {
    val height = LocalSize.current.height
    val items = listOf(
        ColorItem(Color.Red, "Red"),
        ColorItem(Color.Yellow, "Yellow"),
        ColorItem(Color.Blue, "Blue")
    )

    val scrollMode = if (Build.VERSION.SDK_INT >= 37) {
        VerticalScrollMode.SnapScrollMatchHeight(height)
    } else {
        VerticalScrollMode.Normal
    }

    LazyColumn(
        verticalScrollMode = scrollMode
    ) {
        items(items) { item ->
            ColorCard(item, height)
        }
    }
}

@Composable
private fun ColorCard(item: ColorItem, height: Dp) {
    Box(
        modifier = GlanceModifier
            .background(item.color)
            .fillMaxWidth()
            .height(height),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = item.name,
            modifier = GlanceModifier.background(Color.White)
        )
    }
}

Xác định SizeMode

Kích thước AppWidget có thể khác nhau tuỳ thuộc vào thiết bị, lựa chọn của người dùng hoặc trình chạy, vì vậy, bạn cần cung cấp bố cục linh hoạt như mô tả trong trang Cung cấp bố cục tiện ích linh hoạt. Glance đơn giản hoá việc này bằng định nghĩa SizeMode và giá trị LocalSize. Các phần sau đây mô tả 3 chế độ.

SizeMode.Single

SizeMode.Single là chế độ mặc định. Điều này cho biết chỉ có một loại nội dung được cung cấp; tức là ngay cả khi kích thước có sẵn của AppWidget thay đổi, kích thước nội dung sẽ không thay đổi.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Single

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the minimum size or resizable
        // size defined in the App Widget metadata
        val size = LocalSize.current
        // ...
    }
}

Khi sử dụng chế độ này, hãy đảm bảo rằng:

  • Giá trị siêu dữ liệu về kích thước tối thiểu và tối đa được xác định đúng cách dựa trên kích thước nội dung.
  • Nội dung đủ linh hoạt trong phạm vi kích thước dự kiến.

Nhìn chung, bạn nên sử dụng chế độ này khi:

a) AppWidget có kích thước cố định hoặc b) AppWidget không thay đổi nội dung khi được đổi kích thước.

SizeMode.Responsive

Chế độ này tương đương với việc cung cấp bố cục thích ứng, cho phép GlanceAppWidget xác định một tập hợp bố cục thích ứng bị giới hạn bởi các kích thước cụ thể. Đối với mỗi kích thước được xác định, nội dung sẽ được tạo và liên kết với kích thước cụ thể khi AppWidget được tạo hoặc cập nhật. Sau đó, hệ thống sẽ chọn một kích thước phù hợp nhất dựa trên kích thước có sẵn.

Ví dụ: trong đích đến AppWidget, bạn có thể xác định 3 kích thước và nội dung của kích thước đó:

class MyAppWidget : GlanceAppWidget() {

    companion object {
        private val SMALL_SQUARE = DpSize(100.dp, 100.dp)
        private val HORIZONTAL_RECTANGLE = DpSize(250.dp, 100.dp)
        private val BIG_SQUARE = DpSize(250.dp, 250.dp)
    }

    override val sizeMode = SizeMode.Responsive(
        setOf(
            SMALL_SQUARE,
            HORIZONTAL_RECTANGLE,
            BIG_SQUARE
        )
    )

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be one of the sizes defined above.
        val size = LocalSize.current
        Column {
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            }
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width >= HORIZONTAL_RECTANGLE.width) {
                    Button("School")
                }
            }
            if (size.height >= BIG_SQUARE.height) {
                Text(text = "provided by X")
            }
        }
    }
}

Trong ví dụ trước, phương thức provideContent được gọi 3 lần và được liên kết với kích thước đã xác định.

  • Trong lệnh gọi đầu tiên, kích thước được đánh giá là 100x100. Nội dung không có nút bổ sung, cũng như văn bản ở trên cùng và dưới cùng.
  • Trong lệnh gọi thứ hai, kích thước được đánh giá là 250x100. Nội dung bao gồm nút bổ sung, nhưng không bao gồm văn bản trên cùng và dưới cùng.
  • Trong lệnh gọi thứ ba, kích thước được đánh giá là 250x250. Nội dung này bao gồm nút bổ sung và cả hai văn bản.

SizeMode.Responsive là sự kết hợp của 2 chế độ còn lại, cho phép bạn xác định nội dung thích ứng trong phạm vi được xác định trước. Nhìn chung, chế độ này hoạt động hiệu quả hơn và cho phép chuyển đổi mượt mà hơn khi AppWidget được đổi kích thước.

Bảng sau đây cho biết giá trị của kích thước, tuỳ thuộc vào SizeMode và kích thước AppWidget hiện có:

Kích thước hiện có 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Single 110 x 110 110 x 110 110 x 110 110 x 110
SizeMode.Exact 105 x 110 203 x 112 72 x 72 203 x 150
SizeMode.Responsive 80 x 100 80 x 100 80 x 100 150 x 120
* Giá trị chính xác chỉ nhằm mục đích minh hoạ.

SizeMode.Exact

SizeMode.Exact tương đương với cung cấp bố cục chính xác, yêu cầu nội dung GlanceAppWidget mỗi khi kích thước AppWidget có sẵn thay đổi (ví dụ: khi người dùng đổi kích thước AppWidget trên màn hình chính).

Ví dụ: trong tiện ích đích đến, bạn có thể thêm một nút bổ sung nếu chiều rộng có sẵn lớn hơn một giá trị nhất định.

class MyAppWidget : GlanceAppWidget() {

    override val sizeMode = SizeMode.Exact

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        // ...

        provideContent {
            MyContent()
        }
    }

    @Composable
    private fun MyContent() {
        // Size will be the size of the AppWidget
        val size = LocalSize.current
        Column {
            Text(text = "Where to?", modifier = GlanceModifier.padding(12.dp))
            Row(horizontalAlignment = Alignment.CenterHorizontally) {
                Button()
                Button()
                if (size.width > 250.dp) {
                    Button("School")
                }
            }
        }
    }
}

Chế độ này linh hoạt hơn các chế độ khác, nhưng có một số điểm hạn chế:

  • Bạn phải tạo lại hoàn toàn AppWidget mỗi khi kích thước thay đổi. Điều này có thể dẫn đến các vấn đề về hiệu suất và giao diện người dùng bị giật khi nội dung phức tạp.
  • Kích thước có sẵn có thể khác nhau tuỳ thuộc vào cách triển khai của trình chạy. Ví dụ: nếu trình chạy không cung cấp danh sách kích thước, thì kích thước tối thiểu có thể sẽ được dùng.
  • Trong các thiết bị trước Android 12, logic tính toán kích thước có thể không hoạt động trong mọi trường hợp.

Nhìn chung, bạn nên sử dụng chế độ này nếu không thể dùng SizeMode.Responsive (tức là không thể dùng một nhóm nhỏ bố cục thích ứng).

Truy cập vào tài nguyên

Sử dụng LocalContext.current để truy cập vào mọi tài nguyên Android, như trong ví dụ sau:

LocalContext.current.getString(R.string.glance_title)

Bạn nên cung cấp trực tiếp mã nhận dạng tài nguyên để giảm kích thước của đối tượng RemoteViews cuối cùng và cho phép các tài nguyên động, chẳng hạn như màu động.

Các thành phần kết hợp và phương thức chấp nhận tài nguyên bằng "trình cung cấp", chẳng hạn như ImageProvider hoặc bằng phương thức nạp chồng như GlanceModifier.background(R.color.blue). Ví dụ:

Column(
    modifier = GlanceModifier.background(R.color.default_widget_background)
) { /**...*/ }

Image(
    provider = ImageProvider(R.drawable.ic_logo),
    contentDescription = "My image",
)

Xử lý văn bản

Glance 1.1.0 có một API để thiết lập kiểu văn bản. Đặt kiểu văn bản bằng cách sử dụng các thuộc tính fontSize, fontWeight hoặc fontFamily của lớp TextStyle.

fontFamily hỗ trợ tất cả phông chữ hệ thống, như trong ví dụ sau, nhưng không hỗ trợ phông chữ tuỳ chỉnh trong ứng dụng:

Text(
    style = TextStyle(
        fontWeight = FontWeight.Bold,
        fontSize = 18.sp,
        fontFamily = FontFamily.Monospace
    ),
    text = "Example Text"
)

Thêm nút kết hợp

Các nút kết hợp được ra mắt trong Android 12. Glance hỗ trợ khả năng tương thích ngược cho các loại nút kết hợp sau:

Mỗi nút kết hợp này đều hiển thị một khung hiển thị có thể nhấp, đại diện cho trạng thái "đã đánh dấu".

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableIntStateOf(0) }

CheckBox(
    checked = isApplesChecked,
    onCheckedChange = { isApplesChecked = !isApplesChecked },
    text = "Apples"
)

Switch(
    checked = isEnabledSwitched,
    onCheckedChange = { isEnabledSwitched = !isEnabledSwitched },
    text = "Enabled"
)

RadioButton(
    checked = isRadioChecked == 1,
    onClick = { isRadioChecked = 1 },
    text = "Checked"
)

Khi trạng thái thay đổi, lambda được cung cấp sẽ được kích hoạt. Bạn có thể lưu trạng thái đánh dấu, như trong ví dụ sau:

class MyAppWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val myRepository = MyRepository.getInstance()

        provideContent {
            val scope = rememberCoroutineScope()

            val saveApple: (Boolean) -> Unit =
                { scope.launch { myRepository.saveApple(it) } }
            MyContent(saveApple)
        }
    }

    @Composable
    private fun MyContent(saveApple: (Boolean) -> Unit) {

        var isAppleChecked by remember { mutableStateOf(false) }

        Button(
            text = "Save",
            onClick = { saveApple(isAppleChecked) }
        )
    }
}

Bạn cũng có thể cung cấp thuộc tính colors cho CheckBox, SwitchRadioButton để tuỳ chỉnh màu sắc của các thuộc tính này:

CheckBox(
    // ...
    colors = CheckboxDefaults.colors(
        checkedColor = ColorProvider(day = colorAccentDay, night = colorAccentNight),
        uncheckedColor = ColorProvider(day = Color.DarkGray, night = Color.LightGray)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked }
)

Switch(
    // ...
    colors = SwitchDefaults.colors(
        checkedThumbColor = ColorProvider(day = Color.Red, night = Color.Cyan),
        uncheckedThumbColor = ColorProvider(day = Color.Green, night = Color.Magenta),
        checkedTrackColor = ColorProvider(day = Color.Blue, night = Color.Yellow),
        uncheckedTrackColor = ColorProvider(day = Color.Magenta, night = Color.Green)
    ),
    checked = isChecked,
    onCheckedChange = { isChecked = !isChecked },
    text = "Enabled"
)

RadioButton(
    // ...
    colors = RadioButtonDefaults.colors(
        checkedColor = ColorProvider(day = Color.Cyan, night = Color.Yellow),
        uncheckedColor = ColorProvider(day = Color.Red, night = Color.Blue)
    ),

)

Các thành phần bổ sung

Glance 1.1.0 bao gồm việc phát hành các thành phần bổ sung, như mô tả trong bảng sau:

Tên Hình ảnh Đường liên kết tham khảo Ghi chú khác
Nút được tô màu nền alt_text Thành phần
Nút có đường viền alt_text Thành phần
Nút biểu tượng alt_text Thành phần Chính / Phụ / Chỉ biểu tượng
Thanh tiêu đề alt_text Thành phần
Scaffold Khung và thanh tiêu đề nằm trong cùng một bản minh hoạ.

Để biết thêm thông tin về các đặc điểm thiết kế, hãy xem các thiết kế thành phần trong bộ công cụ thiết kế này trên Figma.

Để biết thêm thông tin về bố cục chuẩn, hãy truy cập vào phần Bố cục chuẩn của tiện ích.