Mem-build UI dengan Glance

Halaman ini menjelaskan cara menangani ukuran dan menyediakan tata letak yang fleksibel dan responsif dengan Glance, menggunakan komponen Glance yang ada.

Gunakan Box, Column, dan Row

Glance memiliki tiga tata letak composable utama:

  • Box: Menempatkan elemen di atas elemen lain. Diterjemahkan menjadi RelativeLayout.

  • Column: Menempatkan elemen-elemen setelah satu sama lain dalam sumbu vertikal. Terjemahkan menjadi LinearLayout dengan orientasi vertikal.

  • Row: Menempatkan elemen-elemen setelah satu sama lain dalam sumbu horizontal. Fungsi ini diterjemahkan menjadi LinearLayout dengan orientasi horizontal.

Glance mendukung objek Scaffold. Tempatkan composable Column, Row, dan Box dalam objek Scaffold tertentu.

Gambar tata letak kolom, baris, dan kotak.
Gambar 1. Contoh tata letak dengan Kolom, Baris, dan Kotak.

Setiap composable ini memungkinkan Anda menentukan perataan vertikal dan horizontal kontennya, serta batasan lebar, tinggi, tebal, atau padding menggunakan pengubah. Selain itu, setiap turunan dapat menentukan pengubahnya untuk mengubah ruang dan penempatan di dalam induk.

Contoh berikut menunjukkan cara membuat Row yang mendistribusikan turunannya secara merata, seperti yang terlihat pada Gambar 1:

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

Row mengisi lebar maksimum yang tersedia, dan karena setiap turunan memiliki bobot yang sama, turunan tersebut berbagi ruang yang tersedia secara merata. Anda dapat menentukan bobot, ukuran, padding, atau perataan yang berbeda untuk menyesuaikan tata letak dengan kebutuhan Anda.

Menggunakan tata letak yang dapat di-scroll

Cara lain untuk menyediakan konten responsif adalah dengan membuatnya dapat di-scroll. Hal ini dapat dilakukan dengan composable LazyColumn. Composable ini memungkinkan Anda menentukan sekumpulan item yang akan ditampilkan di dalam penampung yang dapat di-scroll di widget aplikasi.

Cuplikan berikut menunjukkan berbagai cara untuk menentukan item di dalam LazyColumn.

Anda dapat memberikan jumlah item:

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

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

Berikan item individual:

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

Berikan daftar atau array item:

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

Anda juga dapat menggunakan kombinasi contoh sebelumnya:

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")
    }
}

Perhatikan bahwa cuplikan sebelumnya tidak menentukan itemId. Menentukan itemId akan membantu meningkatkan performa serta mempertahankan posisi scroll dalam daftar dan update appWidget mulai Android 12 dan seterusnya (misalnya, saat menambahkan atau menghapus item dari daftar). Contoh berikut menunjukkan cara menentukan itemId:

items(items = peopleList, key = { person -> person.id }) { person ->
    Text(person.name)
}

Menentukan SizeMode

Ukuran AppWidget dapat berbeda bergantung pada perangkat, pilihan pengguna, atau peluncur, jadi penting untuk menyediakan tata letak yang fleksibel seperti yang dijelaskan di halaman Menyediakan tata letak widget yang fleksibel. Glance menyederhanakan hal ini dengan definisi SizeMode dan nilai LocalSize. Bagian berikut menjelaskan tiga mode tersebut.

SizeMode.Single

SizeMode.Single adalah mode default. Ini menunjukkan bahwa hanya satu jenis konten yang disediakan; yaitu, meskipun ukuran AppWidget yang tersedia berubah, ukuran konten tidak berubah.

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
        // ...
    }
}

Saat menggunakan mode ini, pastikan bahwa:

  • Nilai metadata ukuran minimum dan maksimum ditentukan dengan benar berdasarkan ukuran konten.
  • Konten cukup fleksibel dalam rentang ukuran yang diharapkan.

Secara umum, Anda harus menggunakan mode ini saat:

a) AppWidget memiliki ukuran tetap, atau b) tidak mengubah kontennya saat diubah ukurannya.

SizeMode.Responsive

Mode ini setara dengan menyediakan tata letak responsif, yang memungkinkan GlanceAppWidget menentukan sekumpulan tata letak responsif yang dibatasi oleh ukuran tertentu. Untuk setiap ukuran yang ditentukan, konten akan dibuat dan dipetakan ke ukuran tertentu saat AppWidget dibuat atau diupdate. Selanjutnya, sistem akan memilih yang paling sesuai berdasarkan ukuran yang tersedia.

Misalnya, dalam AppWidget tujuan, Anda dapat menentukan tiga ukuran dan kontennya:

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")
            }
        }
    }
}

Pada contoh sebelumnya, metode provideContent dipanggil tiga kali dan dipetakan ke ukuran yang ditentukan.

  • Pada panggilan pertama, ukurannya bernilai 100x100. Konten tidak menyertakan tombol tambahan, atau teks atas dan bawah.
  • Pada panggilan kedua, ukurannya bernilai 250x100. Konten mencakup tombol tambahan, tetapi tidak mencakup teks atas dan bawah.
  • Pada panggilan ketiga, ukurannya bernilai 250x250. Konten tersebut mencakup tombol tambahan dan kedua teks.

SizeMode.Responsive adalah kombinasi dari dua mode lainnya, dan memungkinkan Anda menentukan konten responsif dalam batas yang telah ditentukan. Secara umum, mode ini berperforma lebih baik dan memungkinkan transisi yang lebih halus saat AppWidget diubah ukurannya.

Tabel berikut menunjukkan nilai ukuran, bergantung pada ukuran SizeMode dan AppWidget yang tersedia:

Ukuran yang tersedia 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
* Nilai tepatnya hanya untuk tujuan demo.

SizeMode.Exact

SizeMode.Exact setara dengan menyediakan tata letak yang tepat, yang meminta konten GlanceAppWidget setiap kali ukuran AppWidget yang tersedia berubah (misalnya, saat pengguna mengubah ukuran AppWidget di layar utama).

Misalnya, di widget tujuan, tombol ekstra dapat ditambahkan jika lebar yang tersedia lebih besar dari nilai tertentu.

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")
                }
            }
        }
    }
}

Mode ini memberikan lebih banyak fleksibilitas daripada yang lain, tetapi memiliki beberapa catatan:

  • AppWidget harus dibuat ulang sepenuhnya setiap kali ukuran berubah. Hal ini dapat menyebabkan masalah performa dan UI melonjak jika konten bersifat kompleks.
  • Ukuran yang tersedia mungkin berbeda bergantung pada implementasi peluncur. Misalnya, jika peluncur tidak menyediakan daftar ukuran, ukuran minimum yang memungkinkan akan digunakan.
  • Di perangkat pra-Android 12, logika penghitungan ukuran mungkin tidak berfungsi di semua situasi.

Secara umum, Anda harus menggunakan mode ini jika SizeMode.Responsive tidak dapat digunakan (yaitu, serangkaian kecil tata letak responsif tidak memungkinkan).

Akses materi

Gunakan LocalContext.current untuk mengakses resource Android apa pun, seperti yang ditunjukkan pada contoh berikut:

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

Sebaiknya berikan ID resource secara langsung untuk mengurangi ukuran objek RemoteViews akhir dan untuk mengaktifkan resource dinamis, seperti warna dinamis.

Composable dan metode menerima resource menggunakan "penyedia", seperti ImageProvider, atau menggunakan metode overload seperti GlanceModifier.background(R.color.blue). Contoh:

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

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

Menangani teks

Glance 1.1.0 menyertakan API untuk menyetel gaya teks Anda. Setel gaya teks menggunakan atribut fontSize, fontWeight, atau fontFamily dari class TextStyle.

fontFamily mendukung semua font sistem, seperti yang ditunjukkan dalam contoh berikut, tetapi font kustom di aplikasi tidak didukung:

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

Menambahkan tombol gabungan

Tombol gabungan diperkenalkan di Android 12. Glance mendukung kompatibilitas mundur untuk jenis tombol gabungan berikut:

Setiap tombol gabungan ini menampilkan tampilan yang dapat diklik yang merepresentasikan status "dicentang".

var isApplesChecked by remember { mutableStateOf(false) }
var isEnabledSwitched by remember { mutableStateOf(false) }
var isRadioChecked by remember { mutableStateOf(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"
)

Saat status berubah, lambda yang disediakan akan dipicu. Anda dapat menyimpan status pemeriksaan, seperti yang ditunjukkan pada contoh berikut:

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) }
        )
    }
}

Anda juga dapat memberikan atribut colors ke CheckBox, Switch, dan RadioButton untuk menyesuaikan warnanya:

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)
    ),

)

Komponen tambahan

Glance 1.1.0 mencakup rilis komponen tambahan, seperti yang dijelaskan dalam tabel berikut:

Nama Gambar Link referensi Catatan tambahan
Tombol Terisi alt_text Komponen
Tombol Outline alt_text Komponen
Tombol Ikon alt_text Komponen Utama / Sekunder / Hanya ikon
Panel Judul alt_text Komponen
Scaffold Scaffold dan Kolom judul berada dalam demo yang sama.

Untuk informasi selengkapnya tentang detail desain, lihat desain komponen dalam kit desain ini di Figma.