Tata letak di Jetpack Compose

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.

fbd450e8eab10338.png

Anda dapat menggunakan komponen tata letak standar guna mem-build UI seperti yang satu ini:

d2c39f3c2416c321.png

@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

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:

ec53715fe31913e6.jpeg

Jika layar di atas tidak muncul, buka File > New > New Project.

Saat membuat project baru, pilih Empty Compose Activity dari template yang tersedia.

a67ba73a4f06b7ac.png

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 (atau build.gradle (Module: YourApplicationName.app)) mengimpor dependensi Compose dan mengaktifkan Android Studio supaya bekerja dengan Compose dengan tanda buildFeatures { 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:

d2c39f3c2416c321.png

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:

bf29f2c3f5d6a27.png

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

84f2bb229d67987b.png

Ada beberapa peningkatan yang ingin kami lakukan di sini:

  1. Kami ingin sedikit pemisahan antara placeholder dan tulisan.
  2. 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:

1542fadc7f68feb2.png

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:

c15a1050b051617f.gif

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:

a1ea4c8e16d61ffa.gif

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:

4c7652fc71ccf8dc.gif

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

b3cb99320ec18268.png

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

ef5893f332864e28.png

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.

fccfb817afa8876e.png

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.

4365ce9b02ec2805.png

Di sini, kami dapat menyesuaikan lebih banyak hal selain dari judulnya:

2decc9ec64c79a84.png

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:

bd1c58d4497f523f.png

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:

54b175d305766292.png

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:

aceda77e27f25fe9.png

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:

6adf05bb92b48b76.png

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:

c93d09851d6560c7.png

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:

b2d81ccec4667ef5.png

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:

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

1c747e54111e28c.gif

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

9c6a666c57a84211.gif

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

9bc52801a90401f3.gif

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 layoutkustom 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 ditempatkan
  • constraints: 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:

4ee1054702073598.png

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:

dccb4473e2ca09c6.png

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:

e69cdb015e4d8abe.png

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:

7a54fe8390fe39d2.png

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:

f1f8c6bb7f12cf1.png

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:

e9861768e4e27dd4.png

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:

555f88fd41e4dff4.png

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 bb4c8dfe4b8debaa.pngatau 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:

cb209bb5edf634d6.png

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:

17da5805d6d8fc91.png

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() (atau createRef()) dan setiap composable dalam ConstraintLayout 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 composable ConstraintLayout 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.

72fcb81ab2c0483c.png

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:

729a1b4c03f1f187.png

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:

a4117576ef1768a2.png

Perhatikan bahwa

  • batasan (dan semua bantuan lainnya) dapat dibuat di body ConstraintLayout, namun tidak di dalam constrainAs.
  • 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()
    }
}

616c19b971811cfa.png

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:

fc41cacd547bbea.png

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:

  1. Teruskan ConstraintSet sebagai parameter ke ConstraintLayout.
  2. Tetapkan referensi yang dibuat di ConstraintSet ke composable menggunakan pengubah layoutId.

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:

835f0b8c9f07cd9.png

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:

d61f179394ded825.png

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:

835f0b8c9f07cd9.png

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