1. Pengantar
Di codelab dasar Jetpack Compose, Anda akan mempelajari cara mem-build UI sederhana dengan Compose menggunakan composable seperti Text
serta composable tata letak yang fleksibel seperti Column
dan Row
yang memungkinkan Anda meletakkan item (masing-masing secara vertikal dan horizontal) pada layar dan mengonfigurasi perataan elemen di dalamnya. Begitu pula sebaliknya, apabila Anda tidak ingin item ditampilkan secara vertikal atau horizontal, Box
memungkinkan Anda meletakkan item di belakang dan/atau di depan elemen lainnya.
Anda dapat menggunakan komponen tata letak standar guna mem-build UI seperti yang satu ini:
@Composable
fun PhotographerProfile(photographer: Photographer) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(...)
Column {
Text(photographer.name)
Text(photographer.lastSeenOnline, ...)
}
}
}
Manfaat dari kemampuan menggunakan ulang dan menyusun yang diperlengkapi dalam Compose, Anda dapat mem-build composable Anda sendiri dengan menggabungkan beberapa bagian yang berbeda yang diperlukan pada level abstraksi yang tepat secara bersamaan dalam fungsi composable yang baru.
Dalam codelab ini, Anda akan mempelajari cara menggunakan level tertinggi abstraksi UI Compose, Desain Material, beserta composable level rendah seperti Layout
yang memungkinkan Anda mengukur dan meletakkan elemen pada layar.
Apabila Anda ingin membuat UI berbasis Desain Material, Compose memiliki composable Komponen material bawaan yang dapat Anda gunakan seperti yang akan kita lihat dalam codelab. Jika tidak ingin menggunakan Desain Material atau ingin mem-build sesuatu yang tidak termasuk dalam spesifikasi Desain Material, Anda juga akan mempelajari cara membuat tata letak kustom.
Yang akan Anda pelajari
Dalam codelab ini, Anda akan mempelajari:
- Cara menggunakan composable Komponen material
- Apa itu pengubah dan cara Anda dapat menggunakannya dalam tata letak
- Cara membuat tata letak kustom Anda
- Kapan Anda memerlukan intrinsik
Prasyarat
- Pengalaman dengan sintaksis Kotlin, termasuk lambda
- Memahami dasar-dasar Compose.
Yang akan Anda butuhkan
2. Memulai project Compose baru
Untuk memulai project Compose yang baru, buka Android Studio Bumblebee dan pilih Mulai project Android Studio yang baru sebagaimana ditunjukkan di bawah ini:
Jika layar di atas tidak muncul, buka File > New > New Project.
Saat membuat project baru, pilih Empty Compose Activity dari template yang tersedia.
Klik Berikutnya dan konfigurasi project Anda sebagaimana biasanya. Pastikan Anda memilih minimumSdkVersion sekurangnya API level 21, yang merupakan dukungan Compose API minimum.
Saat memilih template Empty Compose Activity, kode berikut akan dibuat untuk Anda dalam project:
- Project sudah dikonfigurasi untuk menggunakan Compose.
- File
AndroidManifest.xml
dibuat - File
app/build.gradle
(ataubuild.gradle (Module: YourApplicationName.app)
) mengimpor dependensi Compose dan mengaktifkan Android Studio supaya bekerja dengan Compose dengan tandabuildFeatures { compose true }
.
android {
...
kotlinOptions {
jvmTarget = '1.8'
useIR = true
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
}
dependencies {
...
implementation "androidx.compose.ui:ui:$compose_version"
implementation 'androidx.activity:activity-compose:1.4.0'
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
...
}
Solusi untuk codelab
Anda dapat memperoleh kode untuk solusi codelab ini dari GitHub:
$ git clone https://github.com/googlecodelabs/android-compose-codelabs
Atau, Anda dapat mendownload repositori sebagai file Zip:
Anda akan menemukan kode solusi di project LayoutsCodelab
. Sebaiknya ikuti codelab ini langkah demi langkah sesuai kemampuan Anda sendiri dan lihat solusi jika diperlukan. Selama codelab, Anda akan melihat cuplikan kode yang harus ditambahkan ke project.
3. Pengubah
Pengubah memungkinkan Anda mendekorasi composable. Anda dapat mengubah perilakunya, penampilannya, menambahkan informasi seperti label aksesibilitas, memproses input pengguna, atau bahkan menambahkan interaksi tingkat tinggi seperti membuat sesuatu dapat di-klik, di-scroll, ditarik, atau di-zoom. Pengubah adalah objek Kotlin reguler. Anda dapat menetapkan pengubah ke beberapa variabel dan menggunakannya kembali. Anda juga dapat merangkai beberapa pengubah satu demi satu untuk menyusunnya.
Mari terapkan tata letak profil yang sudah ditampilkan di bagian pengantar:
Buka MainActivity.kt
dan tambahkan hal berikut:
@Composable
fun PhotographerCard() {
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
// LocalContentAlpha is defining opacity level of its children
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
@Preview
@Composable
fun PhotographerCardPreview() {
LayoutsCodelabTheme {
PhotographerCard()
}
}
Dengan pratinjau:
Berikutnya, sementara gambar sedang dimuat, Anda mungkin ingin placeholder muncul. Oleh karena itu, Anda dapat menggunakan Surface
tempat kami menentukan bentuk lingkaran dan warna placeholder. Untuk menentukan seberapa besar seharusnya ukurannya, kami dapat menggunakan pengubah size
:
@Composable
fun PhotographerCard() {
Row {
Surface(
modifier = Modifier.size(50.dp),
shape = CircleShape,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
) {
// Image goes here
}
Column {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
}
Ada beberapa peningkatan yang ingin kami lakukan di sini:
- Kami ingin sedikit pemisahan antara placeholder dan tulisan.
- Kami ingin tulisan diletakkan di tengah secara vertikal.
Untuk #1, kami dapat menggunakan Modifier.padding
di Column
yang berisi teks untuk menambahkan sejumlah ruang di start
composable guna memisahkan gambar dan tulisan. Untuk #2, beberapa tata letak menawarkan pengubah yang hanya dapat dipakai di tata letak tersebut dan karakteristik tata letaknya. Contohnya, composable di Row
dapat mengakses pengubah tertentu (dari penerima RowScope
konten Baris) yang cocok seperti weight
atau align
. Cakupan ini menawarkan keamanan jenis, jadi Anda tidak dapat secara tidak sengaja menggunakan pengubah yang tidak dapat dimengerti di tata letak lain, misalnya weight
tidak cocok di Box
, sehingga akan dicegah sebagai kesalahan waktu kompilasi.
@Composable
fun PhotographerCard() {
Row {
Surface(
modifier = Modifier.size(50.dp),
shape = CircleShape,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
) {
// Image goes here
}
Column(
modifier = Modifier
.padding(start = 8.dp)
.align(Alignment.CenterVertically)
) {
Text("Alfred Sisley", fontWeight = FontWeight.Bold)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("3 minutes ago", style = MaterialTheme.typography.body2)
}
}
}
}
Dengan pratinjau:
Sebagian besar composable menerima parameter pengubah opsional untuk membuatnya lebih fleksibel, memungkinkan pemanggil untuk memodifikasinya. Jika Anda membuat composable sendiri, pertimbangkan untuk memiliki pengubah sebagai parameter, default ke Modifier
(yaitu pengubah kosong yang tidak melakukan apa-apa) dan menerapkannya ke composable root dari fungsi Anda. Dalam hal ini:
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier) { ... }
}
Pentingnya urutan pengubah
Dalam kode, perhatikan cara Anda dapat merangkai beberapa pengubah satu demi satu dengan menggunakan fungsi ekstensi factory (mis. Modifier.padding(start = 8.dp).align(Alignment.CenterVertically)
).
Berhati-hatilah saat merangkai pengubah karena urutannya penting. Saat pengubah digabungkan menjadi satu argumen, urutannya memengaruhi hasil akhir.
Jika ingin membuat profil Fotografer dapat diklik dan memiliki beberapa padding, Anda dapat melakukan sesuatu seperti ini:
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.padding(16.dp)
.clickable(onClick = { /* Ignoring onClick */ })
) {
...
}
}
Menggunakan pratinjau interaktif atau menjalankannya di emulator:
Perhatikan cara seluruh areanya tidak dapat diklik! Ini karena padding
diterapkan sebelum pengubah clickable
. Jika Anda menerapkan pengubah padding
setelah clickable
, maka padding disertakan dalam area yang dapat diklik:
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
...
}
}
Menggunakan pratinjau interaktif atau menjalankannya di emulator:
Biarkan imajinasi Anda membumbung tinggi! Pengubah memungkinkan Anda mengubah composable dengan cara yang sangat fleksibel. Misalnya, jika Anda ingin menambahkan beberapa spasi luar, ubah warna latar belakang composable, dan bulatkan sudut Row
, Anda dapat menggunakan kode berikut:
@Composable
fun PhotographerCard(modifier: Modifier = Modifier) {
Row(modifier
.padding(8.dp)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colors.surface)
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
...
}
}
Menggunakan pratinjau interaktif atau menjalankannya di emulator:
Kita akan melihat lebih banyak tentang cara pengubah bekerja di balik layar nanti di codelab.
4. Slot API
Compose menyediakan composable Komponen Material tingkat tinggi yang dapat Anda gunakan untuk mem-build UI. Sementara Compose sedang mem-build blok untuk membuat UI, Anda masih perlu memberikan informasi tentang apa yang akan ditampilkan di layar.
Slot API adalah pola yang diperkenalkan Compose untuk menghadirkan lapisan penyesuaian di atas composable, dalam kasus penggunaan ini, composable Komponen Material yang tersedia.
Mari kita lihat melalui sebuah contoh:
Apabila Anda berpikir tentang Tombol Material, terdapat serangkaian panduan mengenai bagaimana seharusnya tampilan Tombol dan isinya, yang dapat kita terjemahkan menjadi API sederhana untuk digunakan:
Button(text = "Button")
Namun, sering kali Anda ingin dapat menyesuaikan komponen dengan lebih baik, lebih dari yang diharapkan. Anda dapat mencoba dan menambahkan parameter untuk setiap elemen individu yang dapat disesuaikan, tetapi upaya tersebut akan dengan cepat membuat Anda kewalahan:
Button(
text = "Button",
icon: Icon? = myIcon,
textStyle = TextStyle(...),
spacingBetweenIconAndText = 4.dp,
...
)
Oleh karena itu, sebagai ganti menambahkan beberapa parameter untuk menyesuaikan komponen dengan cara yang tidak cocok diterapkan, kami menambahkan Slot. Slot memberikan ruang kosong di UI untuk diisi developer, sesuai keinginan mereka.
Misalnya dalam kasus Tombol, kami dapat membiarkan bagian dalam Tombol diisi oleh Anda, yang mungkin ingin menyisipkan baris dengan ikon dan tulisan:
Button {
Row {
MyImage()
Spacer(4.dp)
Text("Button")
}
}
Untuk mengaktifkan ini, kami menyediakan API untuk Tombol yang mengambil lambda composable turunan ( content: @Composable () -> Unit
). Ini memungkinkan Anda menentukan composable Anda sendiri untuk dipancarkan di dalam Tombol.
@Composable
fun Button(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
...
content: @Composable () -> Unit
)
Perhatikan bahwa lambda ini, yang kami beri nama content
, adalah parameter terakhir. Parameter ini memungkinkan Anda menggunakan sintaksis lambda akhir untuk memasukkan konten ke dalam Tombol dengan cara yang terstruktur.
Compose sangat menggunakan Slot dalam komponen yang lebih kompleks seperti Panel Aplikasi Teratas.
Di sini, kami dapat menyesuaikan lebih banyak hal selain dari judulnya:
Contoh penggunaan:
TopAppBar(
title = {
Text(text = "Page title", maxLines = 2)
},
navigationIcon = {
Icon(myNavIcon)
}
)
Saat membuat composable Anda sendiri, Anda dapat menggunakan pola API Slot untuk membuatnya lebih dapat digunakan kembali.
Di bagian berikutnya, kami akan melihat berbagai composable Komponen Material yang tersedia dan cara menggunakannya saat membuat aplikasi Android.
5. Komponen Material
Compose dilengkapi dengan composable Komponen Material bawaan yang dapat Anda gunakan untuk membuat aplikasi Anda. Composable level paling tinggi adalah Scaffold
.
Scaffold
Scaffold
memungkinkan Anda menerapkan UI dengan struktur tata letak Desain Material dasar. Scaffold menyediakan slot untuk komponen Material level atas yang paling umum, seperti TopAppBar, BottomAppBar, FloatingActionButton, dan Drawer. Dengan Scaffold
, Anda memastikan komponen ini akan diposisikan dan bekerja sama dengan benar.
Berdasarkan template Android Studio yang dihasilkan, kami akan memodifikasi kode contoh untuk menggunakan Scaffold
. Buka MainActivity.kt
dan jangan ragu untuk menghapus composable Greeting
dan GreetingPreview
karena tidak akan digunakan.
Buat composable baru bernama LayoutsCodelab
yang akan kami modifikasi di seluruh codelab:
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.codelab.layouts.ui.LayoutsCodelabTheme
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LayoutsCodelabTheme {
LayoutsCodelab()
}
}
}
}
@Composable
fun LayoutsCodelab() {
Text(text = "Hi there!")
}
@Preview
@Composable
fun LayoutsCodelabPreview() {
LayoutsCodelabTheme {
LayoutsCodelab()
}
}
Jika melihat fungsi pratinjau Compose yang harus dianotasi dengan @Preview
, Anda akan melihat LayoutsCodelab
seperti ini:
Mari kita tambahkan composable Scaffold
untuk contoh sehingga Anda dapat memiliki struktur Desain Material yang khas. Semua parameter dalam Scaffold API
bersifat opsional kecuali isi yang merupakan jenis @Composable (InnerPadding) -> Unit
: lambda menerima padding sebagai parameter. Itulah padding yang harus diterapkan ke composable root isi untuk membatasi item dengan tepat di layar. Untuk memulai yang sederhana, mari kita tambahkan Scaffold
tanpa komponen Material lainnya:
@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
Text(text = "Hi there!", modifier = Modifier.padding(innerPadding))
}
}
Dengan pratinjau:
Jika ingin memiliki Column
dengan konten utama layar, kami harus menerapkan pengubah ke Column
:
@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}
}
Dengan pratinjau:
Untuk membuat kode lebih dapat digunakan kembali dan diuji, kami harus menyusunnya menjadi potongan-potongan kecil. Untuk itu, mari buat fungsi composable lain dengan konten layar.
@Composable
fun LayoutsCodelab() {
Scaffold { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}
Biasanya kami melihat AppBar teratas di aplikasi Android dengan informasi tentang layar, navigasi, dan tindakan saat ini. Mari tambahkan hal-hal tersebut ke contoh sekarang.
TopAppBar
Scaffold
memiliki slot untuk AppBar teratas dengan parameter topBar
dari @Composable () -> Unit
jenis, artinya kami dapat mengisi slot dengan composable apa pun yang diinginkan. Misalnya, jika hanya ingin mengisi teks gaya h3
, kami dapat menggunakan Text
di slot yang disediakan sebagai berikut:
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
Text(
text = "LayoutsCodelab",
style = MaterialTheme.typography.h3
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
Dengan pratinjau:
Namun, untuk sebagian besar komponen Material, Compose hadir dengan TopAppBar
composable yang memiliki slot untuk judul, ikon navigasi, dan tindakan. Compose juga hadir dengan beberapa default yang menyesuaikan dengan apa yang direkomendasikan oleh spesifikasi Material seperti warna yang akan digunakan pada setiap komponen.
Mengikuti pola API slot, kami ingin slot title
dari TopAppBar
memuat Text
dengan judul layar:
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
Dengan pratinjau:
AppBars teratas biasanya memiliki beberapa item tindakan. Dalam contoh, kami akan menambahkan tombol favorit yang dapat Anda ketuk ketika merasa telah mempelajari sesuatu. Compose juga hadir dengan sejumlah Ikon Material yang sudah ditentukan sebelumnya dan dapat Anda gunakan, misalnya ikon tutup, favorit, dan menu.
Slot untuk item tindakan di AppBar atas adalah parameter actions
yang secara internal menggunakan Row
, jadi beberapa tindakan akan ditempatkan secara horizontal. Untuk menggunakan salah satu ikon yang telah ditentukan, kami dapat menggunakan composable IconButton
dengan Icon
di dalamnya:
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
Dengan pratinjau:
Biasanya, bagaimanapun juga, tindakan mengubah status aplikasi Anda. Untuk informasi selengkapnya tentang status, Anda dapat mempelajari dasar-dasar manajemen status di codelab Compose Dasar.
Meletakkan pengubah
Setiap kali kami membuat composable baru, memiliki parameter modifier
yang default ke Modifier
adalah praktik yang baik untuk membuat composable lebih dapat digunakan kembali. Composable BodyContent
sudah menggunakan pengubah sebagai parameter. Jika ingin menambahkan lebih banyak padding ekstra untuk BodyContent
, di mana seharusnya pengubah padding
diletakkan?
Ada dua kemungkinan:
- Terapkan pengubah ke satu-satunya turunan langsung di dalam composable sehingga semua panggilan ke
BodyContent
menerapkan padding tambahan:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(8.dp)) {
Text(text = "Hi there!")
Text(text = "Thanks for going through the Layouts codelab")
}
}
- Terapkan pengubah saat memanggil composable yang akan menambahkan padding tambahan saat dibutuhkan:
@Composable
fun LayoutsCodelab() {
Scaffold(...) { innerPadding ->
BodyContent(Modifier.padding(innerPadding).padding(8.dp))
}
}
Memutuskan tempat penerapan tersebut sangat tergantung pada jenis composable dan kasus penggunaannya. Jika pengubah bersifat intrinsik pada composable, letakkan di dalam; jika tidak, letakkan di luar. Dalam kasus kami, kami akan memilih opsi 2 karena padding adalah sesuatu yang mungkin tidak selalu kami terapkan setiap kali memanggil BodyContent
, padding harus diterapkan berdasarkan kasus per kasus.
Pengubah dapat dirantai dengan memanggil setiap fungsi pengubah berturut-turut setelah yang sebelumnya. Ketika tidak ada metode perantaian yang tersedia, Anda dapat menggunakan .then()
. Dalam contoh kami, kami mulai dengan modifier
(huruf kecil), artinya rantai di-build di atas rantai yang diteruskan sebagai parameter.
Ikon lainnya
Terlepas dari ikon yang di-listing sebelumnya, Anda dapat menggunakan listingan lengkap Ikon Material dengan menambahkan dependensi baru ke project. Jika Anda ingin bereksperimen dengan ikon-ikon itu, buka file app/build.gradle
(atau build.gradle (Module: app)
) dan impor dependensi ui-material-icons-extended
:
dependencies {
...
implementation "androidx.compose.material:material-icons-extended:$compose_version"
}
Silakan dan jangan ragu untuk mengubah ikon TopAppBar
sebanyak yang Anda suka.
Tugas lebih lanjut
Scaffold
dan TopAppBar
hanyalah beberapa composable yang dapat digunakan untuk membuat aplikasi yang terlihat seperti Material. Hal yang sama dapat dilakukan untuk komponen Material lainnya seperti BottomNavigation
atau BottomDrawer
. Sebagai latihan, kami mengajak Anda untuk mencoba mengisi slot Scaffold
dengan API tersebut menggunakan cara yang sama seperti yang kami lakukan sampai sekarang.
6. Menangani listingan
Menampilkan listingan item merupakan pola yang umum dalam aplikasi. Jetpack Compose membuat pola ini mudah diterapkan dengan composable Column
dan Row
, tetapi juga menawarkan listingan lambat yang hanya menyusun dan merangkai item yang saat ini terlihat.
Mari berlatih dengan membuat listingan vertikal dengan 100 item menggunakan composable Column
:
@Composable
fun SimpleList() {
Column {
repeat(100) {
Text("Item #$it")
}
}
}
Karena Column
tidak menangani scroll secara default, beberapa item tidak terlihat karena berada di luar layar. Tambahkan pengubah verticalScroll
untuk mengaktifkan scroll di dalam Column
:
@Composable
fun SimpleList() {
// We save the scrolling position with this state that can also
// be used to programmatically scroll the list
val scrollState = rememberScrollState()
Column(Modifier.verticalScroll(scrollState)) {
repeat(100) {
Text("Item #$it")
}
}
}
Listingan lambat
Column
merender semua item listingan, bahkan yang tidak terlihat di layar, yang merupakan masalah performa ketika ukuran listing semakin besar. Untuk menghindari masalah ini, gunakan LazyColumn
, yang hanya merender item yang terlihat di layar, memungkinkan peningkatan performa, dan tidak perlu pengubah scroll
.
LazyColumn
memiliki DSL untuk menggambarkan isi listingan-nya. Anda akan menggunakan items
, yang dapat menggunakan angka sebagai ukuran listingan. Ini juga mendukung array dan listingan (baca selengkapnya di bagian dokumentasi Listingan).
@Composable
fun LazyList() {
// We save the scrolling position with this state that can also
// be used to programmatically scroll the list
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(100) {
Text("Item #$it")
}
}
}
Menampilkan gambar
Seperti yang telah kita lihat sebelumnya dengan PhotographCard
, Image
adalah composable yang dapat Anda gunakan untuk menampilkan Bitmap atau gambar vektor. Jika gambar diambil dari jarak jauh, prosesnya melibatkan lebih banyak langkah karena aplikasi Anda perlu mendownload aset, men-dekodekannya ke bitmap, dan akhirnya merendernya dalam Image
.
Untuk menyederhanakan langkah-langkah tersebut, Anda akan menggunakan pustaka Coil, yang menyediakan composable yang menjalankan tugas-tugas ini secara efisien.
Tambahkan dependensi Coil di file build.gradle
project Anda:
// build.gradle
implementation 'io.coil-kt:coil-compose:1.4.0'
Saat kami akan mengambil gambar jarak jauh, tambahkan izin INTERNET
ke file manifes Anda:
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
Sekarang, buat composable item tempat Anda akan menampilkan gambar dengan indeks item di sebelahnya:
@Composable
fun ImageListItem(index: Int) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberImagePainter(
data = "https://developer.android.com/images/brand/Android_Robot.png"
),
contentDescription = "Android Logo",
modifier = Modifier.size(50.dp)
)
Spacer(Modifier.width(10.dp))
Text("Item #$index", style = MaterialTheme.typography.subtitle1)
}
}
Berikutnya, tukar composable Text
dalam listingan Anda dengan ImageListItem
ini:
@Composable
fun ImageList() {
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(100) {
ImageListItem(it)
}
}
}
Scroll listingan
Sekarang, mari kita kontrol secara manual posisi scroll listingan. Kami akan menambahkan dua tombol yang memungkinkan Anda untuk men-scroll dengan lancar ke atas dan bawah listingan. Untuk menghindari pemblokiran rendering listingan saat Anda men-scroll, API scroll adalah fungsi penangguhan. Oleh karena itu, kita perlu memanggil API scroll dalam coroutine. Untuk melakukannya, kita dapat membuat CoroutineScope
menggunakan fungsi rememberCoroutineScope
untuk membuat coroutine dari pengendali peristiwa tombol. CoroutineScope
ini akan mengikuti siklus proses situs panggilan. Untuk informasi selengkapnya tentang siklus proses composable, coroutine, dan efek samping, lihat panduan ini.
val listSize = 100
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
// We save the coroutine scope where our animated scroll will be executed
val coroutineScope = rememberCoroutineScope()
Akhirnya, kami menambahkan tombol kami yang akan mengontrol scroll:
Row {
Button(onClick = {
coroutineScope.launch {
// 0 is the first item index
scrollState.animateScrollToItem(0)
}
}) {
Text("Scroll to the top")
}
Button(onClick = {
coroutineScope.launch {
// listSize - 1 is the last index of the list
scrollState.animateScrollToItem(listSize - 1)
}
}) {
Text("Scroll to the end")
}
}
Kode lengkap untuk bagian ini
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import coil.compose.rememberImagePainter
import kotlinx.coroutines.launch
@Composable
fun ImageListItem(index: Int) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberImagePainter(
data = "https://developer.android.com/images/brand/Android_Robot.png"
),
contentDescription = "Android Logo",
modifier = Modifier.size(50.dp)
)
Spacer(Modifier.width(10.dp))
Text("Item #$index", style = MaterialTheme.typography.subtitle1)
}
}
@Composable
fun ScrollingList() {
val listSize = 100
// We save the scrolling position with this state
val scrollState = rememberLazyListState()
// We save the coroutine scope where our animated scroll will be executed
val coroutineScope = rememberCoroutineScope()
Column {
Row {
Button(onClick = {
coroutineScope.launch {
// 0 is the first item index
scrollState.animateScrollToItem(0)
}
}) {
Text("Scroll to the top")
}
Button(onClick = {
coroutineScope.launch {
// listSize - 1 is the last index of the list
scrollState.animateScrollToItem(listSize - 1)
}
}) {
Text("Scroll to the end")
}
}
LazyColumn(state = scrollState) {
items(listSize) {
ImageListItem(it)
}
}
}
}
7. Membuat tata letak kustom Anda sendiri
Compose mempromosikan penggunaan kembali composable sebagai potongan kecil yang cukup untuk beberapa tata letak khusus dengan menggabungkan composable bawaan seperti Column
, Row
, atau Box
.
Namun, Anda mungkin perlu membuat sesuatu yang unik untuk aplikasi Anda yang memerlukan pengukuran dan penataan turunan secara manual. Untuk itu, Anda dapat menggunakan composable Layout
. Sebenarnya semua tata letak level yang lebih tinggi seperti Column
dan Row
di-build dengan composable ini.
Sebelum menyelami cara membuat tata letak kustom, kita perlu mengetahui lebih banyak tentang prinsip Tata Letak di Compose.
Prinsip-prinsip tata letak di Compose
Beberapa fungsi composable memancarkan UI saat dipanggil, yang ditambahkan ke hierarki UI yang akan dirender di layar. Setiap emisi (atau elemen) memiliki satu induk dan kemungkinan banyak turunan. Juga memiliki lokasi di dalam induknya: posisi (x, y), dan ukuran: width
dan height
.
Elemen diminta untuk mengukur kapasitasnya sendiri dengan Batasan yang harus dipenuhi. Batasan membatasi width
dan height
minimum dan maksimum dari sebuah elemen. Jika suatu elemen memiliki elemen turunan, elemen tersebut dapat mengukur setiap turunan untuk membantu menentukan ukurannya sendiri. Setelah elemen melaporkan ukurannya sendiri, elemen tersebut memiliki kesempatan untuk menempatkan elemen turunannya relatif terhadap dirinya sendiri. Ini akan dijelaskan lebih lanjut saat membuat tata letak kustom.
UI Compose tidak mengizinkan pengukuran multi-pass. Artinya, elemen tata letak mungkin tidak mengukur turunannya lebih dari satu kali untuk mencoba konfigurasi pengukuran yang berbeda. Pengukuran single-pass baik untuk performa, memungkinkan Compose menangani hierarki UI yang dalam secara efisien. Jika elemen tata letak mengukur turunannya dua kali dan turunan itu mengukur salah satu turunannya dua kali dan seterusnya, satu upaya untuk membuat tata letak seluruh UI harus melakukan banyak pekerjaan, sehingga sulit untuk menjaga performa aplikasi Anda tetap baik. Namun, ada kalanya Anda benar-benar membutuhkan informasi tambahan selain apa yang didapatkan oleh pengukuran satu turunan - untuk kasus ini ada cara untuk melakukannya, kita akan membicarakannya nanti.
Menggunakan pengubah tata letak
Gunakan pengubah layout
untuk mengontrol cara mengukur dan memosisikan elemen secara manual. Biasanya, struktur umum dari pengubah layout
kustom adalah sebagai berikut:
fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
...
})
Ketika menggunakan pengubah layout
, Anda akan mendapatkan dua parameter lambda:
measurable
: turunan yang akan diukur dan ditempatkanconstraints
: minimum dan maksimum untuk lebar dan tinggi turunan
Misalnya Anda ingin menampilkan Text
di layar dan mengontrol jarak dari atas ke dasar pengukuran baris pertama teks. Untuk itu, Anda harus menempatkan composable secara manual di layar menggunakan pengubah layout
. Lihat perilaku yang diinginkan pada gambar berikutnya dengan jarak dari atas ke dasar pengukuran pertama adalah 24.dp
:
Mari membuat pengubah firstBaselineToTop
terlebih dahulu:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
...
}
)
Hal pertama yang harus dilakukan adalah mengukur composable. Seperti yang kami sebutkan di Prinsip Tata Letak di bagian Compose, Anda hanya dapat mengukur turunan sekali.
Ukur komposisi dengan memanggil measurable.measure(constraints)
. Ketika memanggil measure(constraints)
, Anda dapat memasukkan batasan yang diberikan dari composable yang tersedia di parameter lambda constraints
atau membuat sendiri. Hasil panggilan measure()
di Measurable
adalah Placeable
yang dapat diposisikan dengan memanggil placeRelative(x, y)
, seperti yang akan kita lakukan nanti.
Untuk kasus penggunaan ini, jangan membatasi pengukuran lebih lanjut, cukup gunakan batasan yang diberikan:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
...
}
)
Setelah composable diukur, Anda perlu menghitung ukurannya dan menentukannya dengan memanggil metode layout(width, height)
yang juga menerima lambda yang digunakan untuk menempatkan konten.
Dalam hal ini, lebar composable akan menjadi width
dari composable yang diukur dan tingginya akan menjadi height
composable dengan ketinggian atas ke dasar pengukuran yang diinginkan dikurangi dasar pengukuran pertama:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
...
}
}
)
Sekarang, Anda dapat memosisikan composable di layar dengan memanggil placeable.placeRelative(x, y)
. Jika Anda tidak memanggil placeRelative
, composable tidak akan terlihat. placeRelative
secara otomatis menyesuaikan posisi placeable berdasarkan layoutDirection
saat ini.
Dalam hal ini, posisi y
teks sesuai dengan padding atas dikurangi posisi dasar pengukuran pertama:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = this.then(
layout { measurable, constraints ->
...
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.placeRelative(0, placeableY)
}
}
)
Untuk memverifikasi ini berfungsi seperti yang diharapkan, Anda dapat menggunakan pengubah ini di Text
seperti yang Anda lihat pada gambar di atas:
@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
LayoutsCodelabTheme {
Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
}
}
@Preview
@Composable
fun TextWithNormalPaddingPreview() {
LayoutsCodelabTheme {
Text("Hi there!", Modifier.padding(top = 32.dp))
}
}
Dengan pratinjau:
Menggunakan composable Tata Letak
Alih-alih mengontrol cara satu composable diukur dan diletakkan di layar, Anda mungkin memiliki kebutuhan yang sama untuk sekelompok composable. Untuk itu, Anda dapat menggunakan composable Layout
untuk mengontrol secara manual cara mengukur dan memosisikan turunan tata letak. Biasanya, struktur umum dari composable yang menggunakan Layout
adalah sebagai berikut:
@Composable
fun CustomLayout(
modifier: Modifier = Modifier,
// custom layout attributes
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
Parameter minimum yang diperlukan untuk CustomLayout
adalah modifier
dan content
; parameter ini kemudian diteruskan ke Layout
. Dalam lambda terakhir dari Layout
(dari jenis MeasurePolicy
), Anda mendapatkan parameter lambda yang sama seperti yang Anda dapatkan dengan pengubah layout
.
Untuk menampilkan cara kerja Layout
, mari kita mulai menerapkan Column
yang sangat mendasar menggunakan Layout
untuk memahami API. Nanti, kita akan mem-build sesuatu yang lebih kompleks untuk menunjukkan fleksibilitas dari composable Layout
.
Menerapkan Kolom dasar
Implementasi kustom kami dari Column
menata item secara vertikal. Selain itu, untuk kesederhanaan, tata letak kami menempati ruang sebanyak mungkin di bagian induknya.
Buat composable baru bernama MyOwnColumn
dan tambahkan struktur umum composable Layout
:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
Seperti sebelumnya, hal pertama yang harus dilakukan adalah mengukur turunan yang hanya dapat diukur sekali. Demikian pula dengan cara kerja pengubah tata letak, di parameter lambda measurables
, Anda mendapatkan semua content
yang dapat diukur dengan memanggil measurable.measure(constraints)
.
Untuk kasus penggunaan ini, Anda tidak akan membatasi tampilan turunan lebih lanjut. Saat mengukur turunan, Anda juga harus melacak width
dan height
maksimum dari setiap baris untuk dapat menempatkannya dengan benar di layar nanti:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each child
measurable.measure(constraints)
}
}
}
Sekarang Anda memiliki listingan turunan yang diukur dalam logika kami, sebelum memosisikannya di layar, Anda perlu menghitung ukuran versi Column
kami. Saat Anda membuat turunan sebesar induknya, ukurannya adalah batasan yang diberikan oleh induknya. Tentukan ukuran Column
dengan memanggil metode layout(width, height)
, yang juga memberi Anda lambda yang digunakan untuk menempatkan turunan:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Measure children - code in the previous code snippet
...
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Place children
}
}
}
Terakhir, kami memosisikan turunan di layar dengan memanggil placeable.placeRelative(x, y)
. Untuk menempatkan turunan secara vertikal, kami melacak koordinat y
tempat kami menempatkan turunan. Kode final MyOwnColumn
terlihat seperti ini:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each child
measurable.measure(constraints)
}
// Track the y co-ord we have placed children up to
var yPosition = 0
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
Cara kerja MyOwnColumn
Perhatikan MyOwnColumn
di layar dengan menggunakannya dalam composable BodyContent
. Ganti konten di dalam BodyContent dengan yang berikut ini:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
MyOwnColumn(modifier.padding(8.dp)) {
Text("MyOwnColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}
Dengan pratinjau:
8. Tata letak kustom yang rumit
Setelah dasar-dasar Layout
dibahas. Mari buat contoh yang lebih kompleks untuk menunjukkan fleksibilitas API. Kita akan mem-build petak bertahap Material Study Owl kustom yang dapat Anda lihat di tengah gambar berikut ini:
Petak bertahap Owl menempatkan item secara vertikal, mengisi kolom pada waktu yang ditentukan n
jumlah baris. Melakukan ini dengan Row
dari Columns
tidak mungkin karena Anda tidak akan mendapatkan tata letak yang bertahap. Melakukan Column
dari Rows
dapat dimungkinkan jika Anda menyiapkan data sehingga ditampilkan secara vertikal.
Namun, tata letak kustom juga memberi Anda kesempatan untuk membatasi ketinggian semua item di petak bertahap. Jadi untuk memiliki kontrol lebih besar atas tata letak dan mempelajari cara membuat tata letak kustom, Anda akan mengukur dan memosisikan turunan Anda sendiri.
Jika Anda ingin membuat petak dapat digunakan kembali pada orientasi yang berbeda, kami dapat mengambil jumlah baris yang ingin ditempatkan sebagai parameter di layar. Karena informasi itu harus datang ketika tata letak dipanggil, kami meneruskannya sebagai parameter:
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
Seperti sebelumnya, hal pertama yang harus dilakukan adalah mengukur turunan. Perlu diingat, Anda hanya dapat mengukur turunan sekali.
Untuk kasus penggunaan kami, kami tidak akan membatasi tampilan turunan lebih lanjut. Saat mengukur turunan, kami juga harus melacak apa itu width
dan height
maksimum dari setiap baris:
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Keep track of the width of each row
val rowWidths = IntArray(rows) { 0 }
// Keep track of the max height of each row
val rowHeights = IntArray(rows) { 0 }
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.mapIndexed { index, measurable ->
// Measure each child
val placeable = measurable.measure(constraints)
// Track the width and max height of each row
val row = index % rows
rowWidths[row] += placeable.width
rowHeights[row] = Math.max(rowHeights[row], placeable.height)
placeable
}
...
}
Sekarang kami memiliki listingan turunan yang diukur dalam logika, sebelum memosisikannya di layar, kami perlu menghitung ukuran petak (width
dan height
lengkap) . Selain itu, karena juga sudah mengetahui tinggi maksimum setiap baris, kami dapat menghitung posisi elemen untuk setiap baris di posisi Y. Kami menyimpan posisi Y di variabel rowY
:
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
...
// Grid's width is the widest row
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth
// Grid's height is the sum of the tallest element of each row
// coerced to the height constraints
val height = rowHeights.sumOf { it }
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// Y of each row, based on the height accumulation of previous rows
val rowY = IntArray(rows) { 0 }
for (i in 1 until rows) {
rowY[i] = rowY[i-1] + rowHeights[i-1]
}
...
}
Terakhir, kami memosisikan turunan di layar dengan memanggil placeable.placeRelative(x, y)
. Dalam kasus penggunaan kami, kami juga melacak koordinat X untuk setiap baris dalam variabel rowX
:
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
...
// Set the size of the parent layout
layout(width, height) {
// x cord we have placed up to, per row
val rowX = IntArray(rows) { 0 }
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(
x = rowX[row],
y = rowY[row]
)
rowX[row] += placeable.width
}
}
}
Menggunakan StaggeredGrid kustom dalam sebuah contoh
Sekarang setelah memiliki tata letak petak kustom yang mengetahui cara mengukur dan memosisikan turunan, mari gunakan di aplikasi. Untuk menyimulasikan chip Owl di petak, kita dapat dengan mudah membuat composable yang melakukan hal serupa:
@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.size(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.width(4.dp))
Text(text = text)
}
}
}
@Preview
@Composable
fun ChipPreview() {
LayoutsCodelabTheme {
Chip(text = "Hi there")
}
}
Dengan pratinjau:
Sekarang, mari kita buat listingan topik yang dapat ditampilkan di BodyContent
dan StaggeredGrid
:
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
StaggeredGrid(modifier = modifier) {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
@Preview
@Composable
fun LayoutsCodelabPreview() {
LayoutsCodelabTheme {
BodyContent()
}
}
Dengan pratinjau:
Perhatikan bahwa kita dapat mengubah jumlah baris petak dan masih berfungsi seperti yang diharapkan:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
StaggeredGrid(modifier = modifier, rows = 5) {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
Dengan pratinjau:
Karena tergantung pada jumlah baris, topik dapat keluar dari layar, kita dapat membuat BodyContent
dapat di-scroll hanya dengan menggabungkan StaggeredGrid
dalam Row
yang dapat di-scroll dan memasukkan pengubah ke dalamnya, bukan StaggeredGrid
.
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(modifier = modifier.horizontalScroll(rememberScrollState())) {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
}
Jika Anda menggunakan tombol Pratinjau Interaktif atau menjalankan aplikasi di perangkat dengan mengetuk tombol jalankan Android Studio, Anda akan melihat cara Anda bisa men-scroll konten secara horizontal.
9. Pengubah tata letak di balik layar
Sekarang, setelah mengetahui dasar-dasar pengubah, cara membuat composable kustom, dan mengukur serta memosisikan turunan secara manual, kita akan lebih memahami cara kerja pengubah di balik layar.
Singkatnya, pengubah memungkinkan Anda menyesuaikan perilaku composable. Anda dapat menggabungkan beberapa pengubah dengan merangkai kesemuanya. Ada beberapa jenis pengubah, tetapi di bagian ini, kita akan berfokus pada LayoutModifier
karena pengubah ini dapat mengubah cara komponen UI diukur dan ditata.
Composable bertanggung jawab atas kontennya sendiri dan konten tersebut tidak boleh diperiksa atau dimanipulasi oleh induknya, kecuali jika penulis composable tersebut mengekspos API eksplisit untuk melakukannya. Demikian pula, pengubah composable mendekorasi yang sudah dimodifikasi dengan cara buram yang sama: pengubah dienkapsulasi.
Menganalisis pengubah
Karena Modifier
dan LayoutModifier
adalah antarmuka publik, Anda dapat membuat pengubah Anda sendiri. Seperti Modifier.padding
yang kita gunakan sebelumnya, mari kita analisis implementasinya untuk memahami pengubah dengan lebih baik.
padding
adalah fungsi yang didukung oleh kelas yang mengimplementasikan antarmuka LayoutModifier
dan akan mengganti metode measure
. PaddingModifier
adalah kelas reguler yang mengimplementasikan equals()
sehingga pengubah dapat dibandingkan di seluruh rekomposisi.
Misalnya, inilah kode sumber cara padding
mengubah ukuran dan batasan elemen yang diterapkan pada kode tersebut:
// How to create a modifier
@Stable
fun Modifier.padding(all: Dp) =
this.then(
PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
)
// Implementation detail
private class PaddingModifier(
val start: Dp = 0.dp,
val top: Dp = 0.dp,
val end: Dp = 0.dp,
val bottom: Dp = 0.dp,
val rtlAware: Boolean,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx()
val vertical = top.roundToPx() + bottom.roundToPx()
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {
if (rtlAware) {
placeable.placeRelative(start.roundToPx(), top.roundToPx())
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
width
elemen yang baru akan menjadi width
turunan + nilai padding awal dan akhir diterapkan ke batasan lebar elemen. height
akan menjadi height
turunan + nilai padding atas dan bawah diterapkan ke batasan ketinggian elemen.
Pentingnya urutan
Seperti yang Anda lihat di bagian pertama, urutan adalah hal penting saat menggabungkan pengubah karena diterapkan pada composable, yang dimodifikasi dari awal hingga yang terbaru, yang berarti bahwa ukuran dan tata letak pengubah di sebelah kiri akan memengaruhi pengubah di sebelah kanan. Ukuran akhir dari composable tergantung pada semua pengubah yang dimasukkan sebagai parameter.
Pertama-tama, pengubah akan memperbarui batasan dari kiri ke kanan, lalu, pengubah mengembalikan ukuran dari kanan ke kiri. Mari kita pahami dengan baik melalui sebuah contoh:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(color = Color.LightGray)
.size(200.dp)
.padding(16.dp)
.horizontalScroll(rememberScrollState())
) {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
}
Pengubah yang diterapkan dengan cara ini menghasilkan pratinjau ini:
Pertama, kita mengubah latar belakang untuk melihat cara pengubah memengaruhi UI, lalu membatasi ukurannya agar memiliki 200.dp
width
dan height
, dan terakhir, menerapkan padding untuk menambahkan beberapa ruang antara teks dan sekitarnya.
Karena batasan disebarkan melalui rantai dari kiri ke kanan, batasan dengan konten Row
yang akan diukur adalah (200-16-16)=168
dp untuk width
dan height
minimum dan maksimum. Ini berarti bahwa ukuran StaggeredGrid
akan tepat 168x168
dp. Oleh karena itu, ukuran akhir dari Row
yang dapat di-scroll, setelah rantai modifySize
dijalankan dari sebelah kanan ke kiri, akan berjumlah 200x200
dp.
Jika mengubah urutan pengubah, untuk menerapkan padding terlebih dahulu kemudian ukurannya, kita mendapatkan UI yang berbeda:
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(color = Color.LightGray, shape = RectangleShape)
.padding(16.dp)
.size(200.dp)
.horizontalScroll(rememberScrollState())
) {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
}
Dengan pratinjau:
Dalam hal ini, batasan Row
dan padding
yang dapat di-scroll awalnya akan dikonversi ke batasan size
untuk mengukur turunan. Oleh karena itu, StaggeredGrid
akan dibatasi ke 200
dp untuk width
dan height
minimum dan maksimum. Ukuran StaggeredGrid
adalah 200x200
dp dan karena ukurannya diubah dari kanan ke kiri, pengubah padding
akan menambah ukuran menjadi (200+16+16)x(200+16+16)=232x232
yang juga akan menjadi ukuran akhir dari Row
.
Arah tata letak
Anda dapat mengubah arah tata letak composable menggunakan tampilan LayoutDirection
.
Jika Anda menempatkan composable secara manual di layar, layoutDirection
adalah bagian dari LayoutScope
dari pengubah layout
atau composable Layout
. Ketika menggunakan layoutDirection
, tempatkan composable menggunakan place
tidak seperti metode placeRelative
, metode itu tidak akan secara otomatis mencerminkan posisi dalam konteks kanan-ke-kiri.
Kode lengkap untuk bagian ini
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.codelab.layouts.ui.LayoutsCodelabTheme
import kotlin.math.max
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
) { innerPadding ->
BodyContent(Modifier.padding(innerPadding))
}
}
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(modifier = modifier
.background(color = Color.LightGray)
.padding(16.dp)
.size(200.dp)
.horizontalScroll(rememberScrollState()),
content = {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
})
}
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Keep track of the width of each row
val rowWidths = IntArray(rows) { 0 }
// Keep track of the max height of each row
val rowHeights = IntArray(rows) { 0 }
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.mapIndexed { index, measurable ->
// Measure each child
val placeable = measurable.measure(constraints)
// Track the width and max height of each row
val row = index % rows
rowWidths[row] += placeable.width
rowHeights[row] = Math.max(rowHeights[row], placeable.height)
placeable
}
// Grid's width is the widest row
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth
// Grid's height is the sum of the tallest element of each row
// coerced to the height constraints
val height = rowHeights.sumOf { it }
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// Y of each row, based on the height accumulation of previous rows
val rowY = IntArray(rows) { 0 }
for (i in 1 until rows) {
rowY[i] = rowY[i - 1] + rowHeights[i - 1]
}
// Set the size of the parent layout
layout(width, height) {
// x co-ord we have placed up to, per row
val rowX = IntArray(rows) { 0 }
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(
x = rowX[row],
y = rowY[row]
)
rowX[row] += placeable.width
}
}
}
}
@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.width(4.dp))
Text(text = text)
}
}
}
@Preview
@Composable
fun ChipPreview() {
LayoutsCodelabTheme {
Chip(text = "Hi there")
}
}
@Preview
@Composable
fun LayoutsCodelabPreview() {
LayoutsCodelabTheme {
LayoutsCodelab()
}
}
10. Tata Letak Batasan
ConstraintLayout
dapat membantu Anda menempatkan composable relatif terhadap yang lain di layar dan merupakan alternatif untuk menggunakan banyak Row
, Column
, dan Box
. ConstraintLayout berguna saat menerapkan tata letak yang lebih besar dengan persyaratan perataan yang lebih rumit.
Anda dapat menemukan dependensi Compose Constraint Layout di file build.gradle
project:
// build.gradle
implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01"
ConstraintLayout
di Compose berfungsi dengan DSL:
- Referensi dibuat menggunakan
createRefs()
(ataucreateRef()
) dan setiap composable dalamConstraintLayout
harus memiliki referensi yang terkait dengannya. - Batasan diberikan menggunakan pengubah
constrainAs
, yang menggunakan referensi sebagai parameter dan memungkinkan Anda menentukan batasannya di lambda body. - Batasan ditentukan menggunakan
linkTo
atau metode berguna lainnya. parent
adalah referensi yang sudah ada dan dapat digunakan untuk menentukan batasan terhadap composableConstraintLayout
itu sendiri.
Mari kita mulai dengan contoh sederhana.
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Create references for the composables to constrain
val (button, text) = createRefs()
Button(
onClick = { /* Do something */ },
// Assign reference "button" to the Button composable
// and constrain it to the top of the ConstraintLayout
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button")
}
// Assign reference "text" to the Text composable
// and constrain it to the bottom of the Button composable
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
})
}
}
@Preview
@Composable
fun ConstraintLayoutContentPreview() {
LayoutsCodelabTheme {
ConstraintLayoutContent()
}
}
Kode ini membatasi bagian atas Button
untuk induk dengan margin 16.dp
dan Text
di bagian bawah Button
, juga dengan margin 16.dp
.
Jika kita ingin memusatkan teks secara horizontal, kita dapat menggunakan fungsi centerHorizontallyTo
yang menentukan start
dan end
dari Text
ke bagian tepi dari parent
:
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
... // Same as before
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
// Centers Text horizontally in the ConstraintLayout
centerHorizontallyTo(parent)
})
}
}
Dengan pratinjau:
Ukuran ConstraintLayout
akan sekecil mungkin untuk menggabungkan isinya. Itulah sebabnya Text
tampak berpusat di sekitar Button
, bukan di induknya. Jika perilaku ukuran lain diinginkan, pengubah ukuran (mis. fillMaxSize
, size
) harus diterapkan pada composable ConstraintLayout
seperti tata letak lainnya di Compose.
Helpers
DSL juga mendukung pembuatan panduan, batasan, dan rantai. Contoh:
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Creates references for the three composables
// in the ConstraintLayout's body
val (button1, button2, text) = createRefs()
Button(
onClick = { /* Do something */ },
modifier = Modifier.constrainAs(button1) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button 1")
}
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button1.bottom, margin = 16.dp)
centerAround(button1.end)
})
val barrier = createEndBarrier(button1, text)
Button(
onClick = { /* Do something */ },
modifier = Modifier.constrainAs(button2) {
top.linkTo(parent.top, margin = 16.dp)
start.linkTo(barrier)
}
) {
Text("Button 2")
}
}
}
Dengan pratinjau:
Perhatikan bahwa
- batasan (dan semua bantuan lainnya) dapat dibuat di body
ConstraintLayout
, namun tidak di dalamconstrainAs
. linkTo
dapat digunakan untuk membatasi dengan panduan dan pembatas, dengan cara kerja yang sama untuk tepi tata letak.
Mengustomisasi dimensi
Secara default, turunan ConstraintLayout
akan diizinkan untuk memilih ukuran yang dibutuhkan untuk menggabungkan konten mereka. Misalnya, ini berarti bahwa Teks dapat keluar dari batas layar apabila teks terlalu panjang:
@Composable
fun LargeConstraintLayout() {
ConstraintLayout {
val text = createRef()
val guideline = createGuidelineFromStart(fraction = 0.5f)
Text(
"This is a very very very very very very very long text",
Modifier.constrainAs(text) {
linkTo(start = guideline, end = parent.end)
}
)
}
}
@Preview
@Composable
fun LargeConstraintLayoutPreview() {
LayoutsCodelabTheme {
LargeConstraintLayout()
}
}
Tentu saja, Anda ingin teks menjadi jeda baris di ruang yang tersedia. Untuk mewujudkannya, kita dapat mengubah perilaku width
dari teks:
@Composable
fun LargeConstraintLayout() {
ConstraintLayout {
val text = createRef()
val guideline = createGuidelineFromStart(0.5f)
Text(
"This is a very very very very very very very long text",
Modifier.constrainAs(text) {
linkTo(guideline, parent.end)
width = Dimension.preferredWrapContent
}
)
}
}
Dengan pratinjau:
Perilaku Dimension
yang tersedia adalah:
preferredWrapContent
- tata letaknya menggabungkan konten, tunduk pada batasan dalam dimensi itu.wrapContent
- tata letaknya menggabungkan konten meskipun batasan tidak memperbolehkannya.fillToConstraints
- tata letaknya akan diperluas untuk mengisi ruang yang ditentukan oleh batasannya dalam dimensi itu.preferredValue
- tata letaknya adalah nilai dp tetap, tunduk pada batasan dalam dimensi itu.value
- tata letaknya adalah nilai dp tetap, terlepas dari batasan dalam dimensi itu
Dimension
tertentu juga dapat dikonversi:
width = Dimension.preferredWrapContent.atLeast(100.dp)
API terpisah
Sejauh ini, dalam contoh, batasan telah ditentukan inline, dengan pengubah dalam composable tempat menerapkan Namun, ada beberapa kasus ketika mempertahankan batasan yang dipisahkan dari tata letak yang diterapkan adalah hal yang bermanfaat: contoh umumnya adalah untuk dengan mudah mengubah batasan berdasarkan konfigurasi layar atau menganimasikan antara 2 set batasan.
Untuk kasus ini, Anda dapat menggunakan ConstraintLayout
dengan cara lain:
- Teruskan
ConstraintSet
sebagai parameter keConstraintLayout
. - Tetapkan referensi yang dibuat di
ConstraintSet
ke composable menggunakan pengubahlayoutId
.
Bentuk API ini diterapkan pada contoh ConstraintLayout
yang pertama ditampilkan di atas, dioptimalkan untuk lebar layar, terlihat seperti ini:
@Composable
fun DecoupledConstraintLayout() {
BoxWithConstraints {
val constraints = if (maxWidth < maxHeight) {
decoupledConstraints(margin = 16.dp) // Portrait constraints
} else {
decoupledConstraints(margin = 32.dp) // Landscape constraints
}
ConstraintLayout(constraints) {
Button(
onClick = { /* Do something */ },
modifier = Modifier.layoutId("button")
) {
Text("Button")
}
Text("Text", Modifier.layoutId("text"))
}
}
}
private fun decoupledConstraints(margin: Dp): ConstraintSet {
return ConstraintSet {
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button) {
top.linkTo(parent.top, margin= margin)
}
constrain(text) {
top.linkTo(button.bottom, margin)
}
}
}
11. Intrinsik
Salah satu aturan Compose adalah Anda seharusnya hanya mengukur turunan satu kali; mengukur turunan dua kali akan memunculkan pengecualian runtime. Namun, ada kalanya Anda memerlukan beberapa informasi tentang turunan Anda sebelum mengukurnya.
Intrinsik memungkinkan Anda membuat kueri turunan sebelum benar-benar diukur.
Ke composable, Anda dapat meminta intrinsicWidth
atau intrinsicHeight
:
(min|max)IntrinsicWidth
: Dengan tinggi ini, berapa lebar minimum/maksimum yang dapat Anda gambar dengan benar.(min|max)IntrinsicHeight
: Dengan lebar ini, berapa tinggi minimum/maksimum yang dapat Anda gambar dengan benar.
Misalnya, jika Anda meminta minIntrinsicHeight
dari Text
dengan width
yang tidak terbatas, variabel ini akan menampilkan height
dari Text
seolah-olah teks digambar dalam satu baris.
Cara kerja intrinsik
Bayangkan kita ingin membuat composable yang menampilkan dua teks di layar yang dipisahkan oleh pemisah seperti ini:
Bagaimana cara melakukannya? Kita dapat memiliki Row
dengan dua Text
di dalamnya yang meluas sebanyak mungkin dan Divider
di tengah. Kita ingin Pemisahnya setinggi Text
tertinggi dan tipis (width = 1.dp
).
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
@Preview
@Composable
fun TwoTextsPreview() {
LayoutsCodelabTheme {
Surface {
TwoTexts(text1 = "Hi", text2 = "there")
}
}
}
Jika melihat pratinjau ini, kita melihat bahwa Pemisah meluas ke seluruh layar dan bukan itu yang kita inginkan:
Hal ini terjadi karena Row
mengukur setiap turunan secara individual dan tinggi Text
tidak dapat digunakan untuk membatasi Divider
. Kita ingin Divider
mengisi ruang yang tersedia dengan ketinggian tertentu. Untuk itu, kita dapat menggunakan pengubah height(IntrinsicSize.Min)
.
height(IntrinsicSize.Min)
mengukur ukuran turunannya yang dipaksa untuk setinggi intrinsik minimum mereka. Karena bersifat berulang, ini akan membuat kueri Row
dan turunannya minIntrinsicHeight
.
Menerapkannya ke kode kita, akan berfungsi seperti yang diharapkan
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier.height(IntrinsicSize.Min)) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(color = Color.Black, modifier = Modifier.fillMaxHeight().width(1.dp))
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
@Preview
@Composable
fun TwoTextsPreview() {
LayoutsCodelabTheme {
Surface {
TwoTexts(text1 = "Hi", text2 = "there")
}
}
}
Dengan pratinjau:
minIntrinsicHeight
baris akan menjadi minIntrinsicHeight
maksimum turunannya. minIntrinsicHeight
pembagi adalah 0 karena tidak menempati ruang jika tidak ada batasan yang ditentukan; minIntrinsicHeight
teks akan menjadi teks yang ditentukan width
spesifik. Oleh karena itu, batasan height
Baris akan menjadi minIntrinsicHeight
maksimum dari Text
. Divider
kemudian akan memperluas height
ke batasan height
yang ditentukan oleh Baris.
Otak-Atik
Kapan pun Anda membuat tata letak kustom, Anda dapat mengubah cara penghitungan intrinsik dengan (min|max)Intrinsic(Width|Height)
dari antarmuka MeasurePolicy
; namun, sebagian besar default harus selalu mencukupi.
Selain itu, Anda dapat memodifikasi intrinsik dengan pengubah yang menimpa metode Density.(min|max)Intrinsic(Width|Height)Of
dari antarmuka Pengubah yang juga memiliki default yang baik.
12. Selamat
Selamat, Anda berhasil menyelesaikan codelab ini.
Solusi untuk codelab
Anda dapat memperoleh kode untuk solusi codelab ini dari GitHub:
$ git clone https://github.com/googlecodelabs/android-compose-codelabs
Atau, Anda dapat mendownload repositori sebagai file Zip:
Apa selanjutnya?
Lihat codelab lain di jalur Compose:
Bacaan lebih lanjut
Aplikasi contoh
- Owl membuat tata letak kustom
- Rally menampilkan grafik dan tabel
- Jetsnack dengan tata letak kustom