Inset jendela di Compose

Platform Android bertanggung jawab untuk menggambar UI sistem, seperti status bar dan menu navigasi. UI sistem ini akan ditampilkan, apa pun aplikasi yang digunakan pengguna. WindowInsets menyediakan informasi tentang UI sistem untuk memastikan aplikasi Anda menggambar di area yang tepat dan UI tidak terhalang oleh UI sistem.

Menuju dari tepi ke tepi untuk menggambar di belakang kolom sistem
Gambar 1. Menggambar dari tepi ke tepi untuk menggambar di belakang kolom sistem

Secara default, UI aplikasi Anda dibatasi untuk ditata dalam UI sistem, seperti status bar dan menu navigasi. Cara ini memastikan konten aplikasi Anda tidak terhalang oleh elemen UI sistem.

Namun, sebaiknya aplikasi ikut menampilkan di area tempat UI sistem juga ditampilkan, yang menghasilkan pengalaman pengguna yang lebih lancar dan memungkinkan aplikasi Anda memanfaatkan sepenuhnya ruang jendela yang tersedia. Hal ini juga memungkinkan aplikasi dianimasikan bersama dengan UI sistem, terutama saat menampilkan dan menyembunyikan keyboard virtual.

Memilih untuk menampilkan konten di wilayah ini dan menampilkan konten di belakang UI sistem disebut layar penuh. Di halaman ini, Anda akan mempelajari berbagai jenis inset, cara memilih untuk beralih dari tepi ke tepi, dan cara menggunakan API inset untuk menganimasikan UI dan menghindari bagian yang mengaburkan di aplikasi Anda.

Dasar-dasar inset

Saat aplikasi berjalan dari tepi ke tepi, Anda harus memastikan bahwa konten dan interaksi penting tidak terhalang oleh UI sistem. Misalnya, jika tombol ditempatkan di belakang menu navigasi, pengguna mungkin tidak dapat mengkliknya.

Ukuran UI sistem dan informasi tentang lokasinya ditentukan melalui inset.

Setiap bagian UI sistem memiliki jenis inset terkait yang mendeskripsikan ukurannya dan tempatnya ditempatkan. Misalnya, inset status bar memberikan ukuran dan posisi status bar, sedangkan inset menu navigasi memberikan ukuran dan posisi menu navigasi. Setiap jenis inset terdiri dari empat dimensi piksel: atas, kiri, kanan, dan bawah. Dimensi ini menentukan seberapa jauh UI sistem diperluas dari sisi jendela aplikasi yang sesuai. Untuk menghindari tumpang-tindih dengan jenis UI sistem tersebut, UI aplikasi harus inset dengan jumlah tersebut.

Jenis inset Android bawaan ini tersedia melalui WindowInsets:

WindowInsets.statusBars

Inset yang menjelaskan status bar. Ini adalah bilah UI sistem atas yang berisi ikon notifikasi dan indikator lainnya.

WindowInsets.statusBarsIgnoringVisibility

Inset status bar saat terlihat. Jika status bar saat ini disembunyikan (karena memasuki mode layar penuh yang imersif), inset status bar utama akan kosong, tetapi inset ini tidak akan kosong.

WindowInsets.navigationBars

Inset yang menjelaskan menu navigasi. Menu ini adalah kolom UI sistem di sisi kiri, kanan, atau bawah perangkat, yang mendeskripsikan taskbar atau ikon navigasi. Setelan ini dapat berubah saat runtime berdasarkan metode navigasi pilihan pengguna dan interaksi dengan taskbar.

WindowInsets.navigationBarsIgnoringVisibility

Inset menu navigasi saat terlihat. Jika menu navigasi saat ini disembunyikan (karena memasuki mode layar penuh yang imersif), inset menu navigasi utama akan kosong, tetapi inset ini tidak akan kosong.

WindowInsets.captionBar

Inset yang menjelaskan dekorasi jendela UI sistem jika berada dalam jendela bentuk bebas, seperti kolom judul atas.

WindowInsets.captionBarIgnoringVisibility

Inset kolom teks saat terlihat. Jika kolom teks saat ini disembunyikan, inset kolom teks utama akan kosong, tetapi inset ini tidak akan kosong.

WindowInsets.systemBars

Gabungan inset kolom sistem, yang mencakup status bar, menu navigasi, dan kolom teks.

WindowInsets.systemBarsIgnoringVisibility

Inset kolom sistem saat terlihat. Jika kolom sistem saat ini disembunyikan (karena memasuki mode layar penuh yang imersif), inset kolom sistem utama akan kosong, tetapi inset ini tidak akan kosong.

WindowInsets.ime

Inset yang menjelaskan jumlah ruang di bagian bawah yang ditempati keyboard virtual.

WindowInsets.imeAnimationSource

Inset yang menjelaskan jumlah ruang yang ditempati keyboard software sebelum animasi keyboard saat ini.

WindowInsets.imeAnimationTarget

Inset yang menjelaskan jumlah ruang yang akan ditempati keyboard software setelah animasi keyboard saat ini.

WindowInsets.tappableElement

Jenis inset yang menjelaskan informasi lebih mendetail tentang UI navigasi, yang memberikan jumlah ruang tempat "ketukan" akan ditangani oleh sistem, bukan aplikasi. Untuk menu navigasi transparan dengan navigasi gestur, beberapa elemen aplikasi dapat diketuk melalui UI navigasi sistem.

WindowInsets.tappableElementIgnoringVisibility

Inset elemen yang dapat diketuk saat terlihat. Jika elemen yang dapat diketuk saat ini disembunyikan (karena memasuki mode layar penuh yang imersif), inset elemen utama yang dapat diketuk akan kosong, tetapi inset ini tidak akan kosong.

WindowInsets.systemGestures

Inset yang mewakili jumlah inset tempat sistem akan menangkap gestur untuk navigasi. Aplikasi dapat menentukan secara manual penanganan gestur ini dalam jumlah terbatas melalui Modifier.systemGestureExclusion.

WindowInsets.mandatorySystemGestures

Subset gestur sistem yang akan selalu ditangani oleh sistem, dan yang tidak dapat dikecualikan melalui Modifier.systemGestureExclusion.

WindowInsets.displayCutout

Inset yang mewakili jumlah spasi yang diperlukan untuk menghindari tumpang-tindih dengan potongan layar (notch atau lubang jarum).

WindowInsets.waterfall

Inset yang mewakili area melengkung dari tampilan waterfall. Tampilan air terjun memiliki area lengkung di sepanjang tepi layar tempat layar mulai memenuhi sisi perangkat.

Jenis ini dirangkum oleh tiga jenis inset "aman" yang memastikan konten tidak tersamarkan:

Jenis inset "aman" ini melindungi konten dengan berbagai cara, berdasarkan inset platform yang mendasarinya:

  • Gunakan WindowInsets.safeDrawing untuk melindungi konten yang tidak boleh digambar di bawah UI sistem. Ini adalah penggunaan inset yang paling umum: untuk mencegah konten menggambar yang terhalang oleh UI sistem (baik sebagian maupun sepenuhnya).
  • Gunakan WindowInsets.safeGestures untuk melindungi konten dengan gestur. Hal ini akan menghindari bentrokan gestur sistem dengan gestur aplikasi (seperti untuk sheet bawah, carousel, atau dalam game).
  • Gunakan WindowInsets.safeContent sebagai kombinasi WindowInsets.safeDrawing dan WindowInsets.safeGestures untuk memastikan konten tidak memiliki tumpang-tindih visual dan tidak ada gestur yang tumpang-tindih.

Penyiapan inset

Untuk memberi aplikasi Anda kontrol penuh atas tempat menggambar konten, ikuti langkah-langkah penyiapan ini. Tanpa langkah-langkah ini, aplikasi Anda mungkin menggambar warna hitam atau solid di belakang UI sistem, atau tidak dianimasikan secara sinkron dengan keyboard virtual.

  1. Panggil enableEdgeToEdge() di Activity.onCreate. Panggilan ini meminta agar aplikasi Anda ditampilkan di belakang UI sistem. Aplikasi Anda kemudian akan dapat mengontrol cara inset tersebut digunakan untuk menyesuaikan UI.
  2. Tetapkan android:windowSoftInputMode="adjustResize" di entri AndroidManifest.xml Aktivitas Anda. Setelan ini memungkinkan aplikasi Anda menerima ukuran IME software sebagai inset, yang dapat Anda gunakan untuk menambahkan dan menata letak konten dengan tepat saat IME muncul dan menghilang di aplikasi Anda.

    <!-- in your AndroidManifest.xml file: -->
    <activity
      android:name=".ui.MainActivity"
      android:label="@string/app_name"
      android:windowSoftInputMode="adjustResize"
      android:theme="@style/Theme.MyApplication"
      android:exported="true">
    

API Compose

Setelah Aktivitas mengambil alih penanganan semua inset, Anda dapat menggunakan Compose API untuk memastikan bahwa konten tidak terhalang dan elemen yang dapat berinteraksi tidak tumpang-tindih dengan UI sistem. API ini juga menyinkronkan tata letak aplikasi Anda dengan perubahan inset.

Misalnya, ini adalah metode paling dasar untuk menerapkan inset ke konten seluruh aplikasi Anda:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    enableEdgeToEdge()

    setContent {
        Box(Modifier.safeDrawingPadding()) {
            // the rest of the app
        }
    }
}

Cuplikan ini menerapkan inset jendela safeDrawing sebagai padding di sekitar seluruh konten aplikasi. Meskipun ini memastikan elemen yang dapat berinteraksi tidak tumpang-tindih dengan UI sistem, ini juga berarti bahwa tidak ada aplikasi yang akan menggambar di belakang UI sistem untuk mencapai efek layar penuh. Untuk memanfaatkan seluruh jendela secara penuh, Anda perlu menyesuaikan tempat inset diterapkan berdasarkan layar per layar atau komponen per komponen.

Semua jenis inset ini dianimasikan secara otomatis dengan animasi IME yang di-backport ke API 21. Dengan demikian, semua tata letak yang menggunakan inset ini juga dianimasikan secara otomatis saat nilai inset berubah.

Ada dua cara utama untuk menggunakan jenis inset ini untuk menyesuaikan tata letak Composable: pengubah padding dan pengubah ukuran inset.

Pengubah Padding

Modifier.windowInsetsPadding(windowInsets: WindowInsets) menerapkan inset jendela tertentu sebagai padding, yang bertindak seperti Modifier.padding. Misalnya, Modifier.windowInsetsPadding(WindowInsets.safeDrawing) menerapkan inset gambar yang aman sebagai padding di 4 sisinya.

Ada juga beberapa metode utilitas bawaan untuk jenis inset yang paling umum. Modifier.safeDrawingPadding() adalah salah satu metode tersebut, yang setara dengan Modifier.windowInsetsPadding(WindowInsets.safeDrawing). Ada pengubah analog untuk jenis inset lainnya.

Pengubah ukuran inset

Pengubah berikut menerapkan jumlah inset jendela dengan menetapkan ukuran komponen menjadi ukuran inset:

Modifier.windowInsetsStartWidth(windowInsets: WindowInsets)

Menerapkan sisi awal windowInsets sebagai lebar (seperti Modifier.width)

Modifier.windowInsetsEndWidth(windowInsets: WindowInsets)

Menerapkan sisi akhir windowInsets sebagai lebar (seperti Modifier.width)

Modifier.windowInsetsTopHeight(windowInsets: WindowInsets)

Menerapkan sisi atas windowInsets sebagai tinggi (seperti Modifier.height)

Modifier.windowInsetsBottomHeight(windowInsets: WindowInsets)

Menerapkan sisi bawah windowInsets sebagai tinggi (seperti Modifier.height)

Pengubah ini sangat berguna untuk mengubah ukuran Spacer yang menghabiskan ruang inset:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

Pemakaian inset

Pengubah padding inset (windowInsetsPadding dan helper seperti safeDrawingPadding) otomatis menggunakan bagian inset yang diterapkan sebagai padding. Saat masuk ke hierarki komposisi lebih dalam, pengubah padding inset bertingkat dan pengubah ukuran inset mengetahui bahwa beberapa bagian inset telah digunakan oleh pengubah padding inset luar, dan menghindari penggunaan bagian inset yang sama lebih dari sekali yang akan menghasilkan terlalu banyak ruang.

Pengubah ukuran inset juga menghindari penggunaan bagian inset yang sama lebih dari sekali jika inset telah digunakan. Namun, karena mengubah ukurannya secara langsung, inset tidak menggunakan inset.

Akibatnya, pengubah padding bertingkat akan otomatis mengubah jumlah padding yang diterapkan ke setiap composable.

Dengan melihat contoh LazyColumn yang sama seperti sebelumnya, LazyColumn sedang diubah ukurannya oleh pengubah imePadding. Di dalam LazyColumn, item terakhir diukur menjadi tinggi bagian bawah kolom sistem:

LazyColumn(
    Modifier.imePadding()
) {
    // Other content
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

Jika IME ditutup, pengubah imePadding() tidak menerapkan padding, karena IME tidak memiliki tinggi. Karena pengubah imePadding() tidak menerapkan padding, tidak ada inset yang digunakan, dan tinggi Spacer akan menjadi ukuran sisi bawah kolom sistem.

Saat IME terbuka, inset IME akan dianimasikan agar sesuai dengan ukuran IME, dan pengubah imePadding() mulai menerapkan padding bawah untuk mengubah ukuran LazyColumn saat IME terbuka. Saat pengubah imePadding() mulai menerapkan padding bawah, pengubah juga mulai menggunakan jumlah inset tersebut. Oleh karena itu, tinggi Spacer mulai menurun, sebagai bagian dari jarak untuk kolom sistem telah diterapkan oleh pengubah imePadding(). Setelah pengubah imePadding() menerapkan jumlah padding bawah yang lebih besar dari kolom sistem, tinggi Spacer adalah nol.

Saat IME ditutup, perubahan terjadi secara terbalik: Spacer mulai memperluas dari ketinggian nol setelah imePadding() diterapkan lebih sedikit dari sisi bawah kolom sistem, hingga akhirnya Spacer cocok dengan tinggi sisi bawah kolom sistem setelah IME sepenuhnya dianimasikan.

Gambar 2. Kolom lambat dari tepi ke tepi dengan TextField

Perilaku ini dicapai melalui komunikasi antara semua pengubah windowInsetsPadding, dan dapat dipengaruhi dengan beberapa cara lainnya.

Modifier.consumeWindowInsets(insets: WindowInsets) juga menggunakan inset dengan cara yang sama seperti Modifier.windowInsetsPadding, tetapi tidak menerapkan inset yang digunakan sebagai padding. Hal ini berguna jika dikombinasikan dengan pengubah ukuran inset, untuk menunjukkan kepada saudara bahwa sejumlah inset tertentu telah digunakan:

Column(Modifier.verticalScroll(rememberScrollState())) {
    Spacer(Modifier.windowInsetsTopHeight(WindowInsets.systemBars))

    Column(
        Modifier.consumeWindowInsets(
            WindowInsets.systemBars.only(WindowInsetsSides.Vertical)
        )
    ) {
        // content
        Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
    }

    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}

Modifier.consumeWindowInsets(paddingValues: PaddingValues) berperilaku sangat mirip dengan versi dengan argumen WindowInsets, tetapi menggunakan PaddingValues arbitrer untuk digunakan. Hal ini berguna untuk memberi tahu turunan saat padding atau spasi disediakan oleh beberapa mekanisme selain pengubah padding inset, seperti Modifier.padding biasa atau pengatur jarak tinggi tetap:

@OptIn(ExperimentalLayoutApi::class)
Column(Modifier.padding(16.dp).consumeWindowInsets(PaddingValues(16.dp))) {
    // content
    Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}

Jika inset jendela mentah diperlukan tanpa konsumsi, gunakan nilai WindowInsets secara langsung, atau gunakan WindowInsets.asPaddingValues() untuk menampilkan PaddingValues inset yang tidak terpengaruh oleh pemakaian. Namun, karena peringatan di bawah ini, sebaiknya gunakan pengubah padding inset jendela dan pengubah ukuran inset jendela jika memungkinkan.

Fase Inset dan Jetpack Compose

Compose menggunakan API inti AndroidX yang mendasarinya untuk mengupdate dan menganimasikan inset, yang menggunakan API platform dasar yang mengelola inset. Karena perilaku platform tersebut, inset memiliki hubungan khusus dengan fase Jetpack Compose.

Nilai inset diperbarui setelah fase komposisi, tetapi sebelum fase tata letak. Ini berarti pembacaan nilai inset dalam komposisi umumnya menggunakan nilai inset yang terlambat satu frame. Pengubah bawaan yang dijelaskan di halaman ini dibuat untuk menunda menggunakan nilai inset hingga fase tata letak, yang memastikan bahwa nilai inset digunakan pada frame yang sama saat diperbarui.

Animasi IME keyboard dengan WindowInsets

Anda dapat menerapkan Modifier.imeNestedScroll() ke penampung scroll untuk membuka dan menutup IME secara otomatis saat men-scroll ke bagian bawah penampung.

class WindowInsetsExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                MyScreen()
            }
        }
    }
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            onClick = { }
        ) {
            Icon(imageVector = Icons.Filled.Add, contentDescription = "Add")
        }
    }
}

Animasi yang menampilkan elemen UI yang menggulir ke atas dan ke bawah untuk memberi jalan bagi keyboard

Gambar 1. Animasi IME

Dukungan inset untuk Komponen Material 3

Untuk kemudahan penggunaan, banyak composable Material 3 bawaan (androidx.compose.material3) yang menangani inset itu sendiri, berdasarkan cara composable tersebut ditempatkan di aplikasi sesuai dengan spesifikasi Material.

Composable penanganan inset

Di bawah ini adalah daftar Komponen Material yang otomatis menangani inset.

Panel aplikasi

Penampung konten

Scaffold

Secara default, Scaffold menyediakan inset sebagai parameter paddingValues untuk Anda gunakan dan gunakan. Scaffold tidak menerapkan inset ke konten; tanggung jawab ini adalah milik Anda. Misalnya, untuk menggunakan inset ini dengan LazyColumn di dalam Scaffold:

Scaffold { innerPadding ->
    // innerPadding contains inset information for you to use and apply
    LazyColumn(
        // consume insets as scaffold doesn't do it by default
        modifier = Modifier.consumeWindowInsets(innerPadding),
        contentPadding = innerPadding
    ) {
        items(count = 100) {
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(50.dp)
                    .background(colors[it % colors.size])
            )
        }
    }
}

Mengganti inset default

Anda dapat mengubah parameter windowInsets yang diteruskan ke composable untuk mengonfigurasi perilaku composable. Parameter ini dapat berupa jenis inset jendela lain untuk diterapkan, atau dinonaktifkan dengan meneruskan instance kosong: WindowInsets(0, 0, 0, 0).

Misalnya, untuk menonaktifkan penanganan inset pada LargeTopAppBar, tetapkan parameter windowInsets ke instance kosong:

LargeTopAppBar(
    windowInsets = WindowInsets(0, 0, 0, 0),
    title = {
        Text("Hi")
    }
)

Interop dengan inset sistem View

Anda mungkin perlu mengganti inset default jika layar memiliki kode View dan Compose dalam hierarki yang sama. Dalam hal ini, Anda harus secara eksplisit menentukan mana yang harus menggunakan inset dan mana yang harus mengabaikannya.

Misalnya, jika tata letak terluar Anda adalah tata letak Android View, Anda harus menggunakan inset dalam sistem View dan mengabaikannya untuk Compose. Atau, jika tata letak terluar Anda adalah composable, Anda harus menggunakan inset di Compose, dan menambahkan composable AndroidView pada composable.

Secara default, setiap ComposeView menggunakan semua inset pada tingkat pemakaian WindowInsetsCompat. Untuk mengubah perilaku default ini, tetapkan ComposeView.consumeWindowInsets ke false.

Referensi

  • Now in Android - aplikasi Android yang berfungsi sepenuhnya dan dibangun sepenuhnya dengan Kotlin dan Jetpack Compose.