Ada beberapa istilah dan konsep yang penting untuk dipahami saat menangani gestur dalam aplikasi. Halaman ini menjelaskan istilah pointer, peristiwa pointer, dan gestur, serta memperkenalkan berbagai tingkat abstraksi untuk gestur. Kursus ini juga membahas lebih dalam tentang konsumsi dan penyebaran peristiwa.
Definisi
Untuk memahami berbagai konsep di halaman ini, Anda perlu memahami beberapa terminologi yang digunakan:
- Penunjuk: Objek fisik yang dapat Anda gunakan untuk berinteraksi dengan aplikasi.
Untuk perangkat seluler, pointer yang paling umum adalah jari Anda yang berinteraksi dengan
layar sentuh. Atau, Anda dapat menggunakan stilus untuk menggantikan jari Anda.
Untuk layar besar, Anda dapat menggunakan mouse atau trackpad untuk berinteraksi secara tidak langsung dengan
layar. Perangkat input harus dapat "menunjuk" ke koordinat agar dianggap sebagai pointer, sehingga keyboard, misalnya, tidak dapat dianggap sebagai pointer. Di Compose, jenis pointer disertakan dalam perubahan pointer menggunakan
PointerType. - Peristiwa pointer: Menjelaskan interaksi level rendah dari satu atau beberapa pointer
dengan aplikasi pada waktu tertentu. Setiap interaksi pointer, seperti meletakkan
jari di layar atau menarik mouse, akan memicu peristiwa. Di
Compose, semua informasi yang relevan untuk peristiwa tersebut terdapat dalam
class
PointerEvent. - Gestur: Urutan peristiwa pointer yang dapat ditafsirkan sebagai satu tindakan. Misalnya, gestur ketuk dapat dianggap sebagai urutan peristiwa tekan yang diikuti dengan peristiwa lepas. Ada gestur umum yang digunakan oleh banyak aplikasi, seperti ketuk, tarik, atau transformasi, tetapi Anda juga dapat membuat gestur kustom sendiri jika diperlukan.
Berbagai tingkat abstraksi
Jetpack Compose menyediakan berbagai tingkat abstraksi untuk menangani gestur.
Di tingkat teratas adalah dukungan komponen. Composable seperti Button
otomatis menyertakan dukungan gestur. Untuk menambahkan dukungan gestur ke komponen
kustom, Anda dapat menambahkan pengubah gestur seperti clickable ke composable
arbitrer. Terakhir, jika memerlukan gestur kustom, Anda dapat menggunakan
pengubah pointerInput.
Sebagai aturan, bangun pada tingkat abstraksi tertinggi yang menawarkan
fungsionalitas yang Anda butuhkan. Dengan cara ini, Anda akan mendapatkan manfaat dari praktik terbaik yang disertakan dalam lapisan. Misalnya, Button berisi lebih banyak informasi semantik, yang digunakan untuk
aksesibilitas, daripada clickable, yang berisi lebih banyak informasi daripada implementasi
pointerInput mentah.
Dukungan komponen
Banyak komponen siap pakai di Compose menyertakan semacam penanganan gestur
internal. Misalnya, LazyColumn merespons gestur tarik dengan
men-scroll kontennya, Button menampilkan ripple saat Anda menekannya,
dan komponen SwipeToDismiss berisi logika geser untuk menutup
elemen. Jenis penanganan gestur ini berfungsi secara otomatis.
Selain penanganan gestur internal, banyak komponen juga mengharuskan pemanggil untuk
menangani gestur. Misalnya, Button otomatis mendeteksi ketukan
dan memicu peristiwa klik. Anda meneruskan lambda onClick ke Button untuk
bereaksi terhadap gestur. Demikian pula, Anda menambahkan lambda onValueChange ke
Slider untuk bereaksi terhadap pengguna yang menarik tuas penggeser.
Jika sesuai dengan kasus penggunaan Anda, pilih gestur yang disertakan dalam komponen, karena gestur tersebut
menyertakan dukungan siap pakai untuk fokus dan aksesibilitas, serta telah
diuji dengan baik. Misalnya, Button ditandai dengan cara khusus sehingga
layanan aksesibilitas mendeskripsikannya dengan benar sebagai tombol, bukan hanya elemen
yang dapat diklik:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
Untuk mempelajari lebih lanjut aksesibilitas di Compose, lihat Aksesibilitas di Compose.
Menambahkan gestur tertentu ke composable arbitrer dengan pengubah
Anda dapat menerapkan pengubah gestur ke composable arbitrer mana pun agar
composable memproses gestur. Misalnya, Anda dapat mengizinkan Box generik
menangani gestur ketuk dengan menjadikannya clickable, atau mengizinkan Column
menangani scroll vertikal dengan menerapkan verticalScroll.
Ada banyak pengubah untuk menangani berbagai jenis gestur:
- Menangani ketukan dan penekanan dengan pengubah
clickable,combinedClickable,selectable,toggleable, dantriStateToggleable. - Menangani scroll dengan pengubah
horizontalScroll,verticalScroll, dan pengubahscrollableyang lebih umum. - Menangani penyeretan dengan pengubah
draggabledanswipeable. - Menangani gestur multi-kontrol seperti menggeser, memutar, dan memperbesar, dengan
pengubah
transformable.
Sebagai aturan, pilih pengubah gestur siap pakai daripada penanganan gestur kustom.
Pengubah menambahkan lebih banyak fungsi selain penanganan peristiwa pointer murni.
Misalnya, pengubah clickable tidak hanya menambahkan deteksi penekanan dan
ketukan, tetapi juga menambahkan informasi semantik, indikasi visual pada interaksi,
pengarahan kursor, fokus, dan dukungan keyboard. Anda dapat memeriksa kode sumber
clickable untuk melihat cara fungsi
ditambahkan.
Menambahkan gestur kustom ke composable arbitrer dengan pengubah pointerInput
Tidak semua gestur diimplementasikan dengan pengubah gestur siap pakai. Misalnya, Anda tidak dapat menggunakan pengubah untuk bereaksi terhadap tarik setelah tekan lama, klik kontrol, atau ketukan tiga jari. Sebagai gantinya, Anda dapat menulis pengendali gestur
sendiri untuk mengidentifikasi gestur kustom ini. Anda dapat membuat pengendali gestur dengan
pengubah pointerInput, yang memberi Anda akses ke peristiwa pointer
mentah.
Kode berikut memproses peristiwa pointer mentah:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
Jika Anda menguraikan cuplikan ini, komponen intinya adalah:
- Pengubah
pointerInput. Anda meneruskan satu atau beberapa kunci ke fungsi tersebut. Saat nilai salah satu kunci tersebut berubah, lambda konten pengubah akan dieksekusi ulang. Contoh ini meneruskan filter opsional ke composable. Jika nilai filter tersebut berubah, pengendali peristiwa pointer harus dijalankan ulang untuk memastikan peristiwa yang tepat dicatat ke dalam log. awaitPointerEventScopemembuat cakupan coroutine yang dapat digunakan untuk menunggu peristiwa pointer.awaitPointerEventmenangguhkan coroutine hingga peristiwa pointer berikutnya terjadi.
Meskipun memproses peristiwa input mentah sangat efektif, menulis gestur kustom berdasarkan data mentah ini juga rumit. Untuk menyederhanakan pembuatan gestur kustom, tersedia banyak metode utilitas.
Mendeteksi gestur penuh
Alih-alih menangani peristiwa pointer mentah, Anda dapat memproses gestur tertentu
yang terjadi dan meresponsnya dengan tepat. AwaitPointerEventScope menyediakan
metode untuk memproses:
- Tekan, ketuk, ketuk dua kali, dan tekan lama:
detectTapGestures - Tarik:
detectHorizontalDragGestures,detectVerticalDragGestures,detectDragGestures, dandetectDragGesturesAfterLongPress - Transformasi:
detectTransformGestures
Ini adalah pendeteksi tingkat teratas, jadi Anda tidak dapat menambahkan beberapa pendeteksi dalam satu
pengubah pointerInput. Cuplikan berikut hanya mendeteksi ketukan, bukan
tarikan:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
Secara internal, metode detectTapGestures memblokir coroutine, dan detektor kedua tidak pernah tercapai. Jika Anda perlu menambahkan lebih dari satu pemroses gestur ke
composable, gunakan instance pengubah pointerInput terpisah:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
Menangani peristiwa per gestur
Menurut definisinya, gestur dimulai dengan peristiwa pointer ke bawah. Anda dapat menggunakan
metode bantuan awaitEachGesture, bukan loop while(true) yang
melewati setiap peristiwa mentah. Metode awaitEachGesture memulai ulang
blok yang memuatnya saat semua pointer telah diangkat, yang menunjukkan bahwa gestur telah
selesai:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
Dalam praktiknya, Anda hampir selalu ingin menggunakan awaitEachGesture kecuali jika Anda
merespons peristiwa pointer tanpa mengidentifikasi gestur. Contohnya adalah
hoverable, yang tidak merespons peristiwa pointer ke bawah atau ke atas— hanya
perlu mengetahui kapan pointer masuk atau keluar dari batasnya.
Menunggu peristiwa atau sub-gestur tertentu
Ada serangkaian metode yang membantu mengidentifikasi bagian umum gestur:
- Tangguhkan hingga pointer turun dengan
awaitFirstDown, atau tunggu hingga semua pointer naik denganwaitForUpOrCancellation. - Buat pemroses tarik tingkat rendah menggunakan
awaitTouchSlopOrCancellationdanawaitDragOrCancellation. Pengendali gestur pertama-tama akan ditangguhkan hingga pointer mencapai slop sentuh, lalu ditangguhkan hingga peristiwa tarik pertama muncul. Jika Anda hanya tertarik pada operasi tarik di sepanjang satu sumbu, gunakanawaitHorizontalTouchSlopOrCancellationplusawaitHorizontalDragOrCancellation, atauawaitVerticalTouchSlopOrCancellationplusawaitVerticalDragOrCancellation. - Tangguhkan hingga terjadi penekanan lama dengan
awaitLongPressOrCancellation. - Gunakan metode
draguntuk terus memproses peristiwa tarik, atauhorizontalDragatauverticalDraguntuk memproses peristiwa tarik pada satu sumbu.
Menerapkan penghitungan untuk peristiwa multi-sentuh
Saat pengguna melakukan gestur multi-kontrol menggunakan lebih dari satu pointer,
akan sulit untuk memahami transformasi yang diperlukan berdasarkan nilai mentah.
Jika pengubah transformable atau metode detectTransformGestures
tidak memberikan kontrol yang cukup mendetail untuk kasus penggunaan Anda, Anda dapat
memproses peristiwa mentah dan menerapkan penghitungan pada peristiwa tersebut. Metode bantuan ini
adalah calculateCentroid, calculateCentroidSize,
calculatePan, calculateRotation, dan calculateZoom.
Pengiriman peristiwa dan hit-testing
Tidak semua peristiwa pointer dikirim ke setiap pengubah pointerInput. Pengiriman peristiwa berfungsi sebagai berikut:
- Peristiwa pointer dikirim ke hierarki composable. Saat pointer baru memicu peristiwa pointer pertamanya, sistem akan mulai melakukan hit-test pada composable yang "memenuhi syarat". Composable dianggap memenuhi syarat jika memiliki kemampuan penanganan input pointer. Hit-testing mengalir dari bagian atas hierarki UI ke bagian bawah. Composable "terkena" saat peristiwa pointer terjadi dalam batas composable tersebut. Proses ini menghasilkan rantai composable yang lulus hit-test.
- Secara default, jika ada beberapa composable yang memenuhi syarat pada tingkat hierarki yang sama, hanya composable dengan indeks z tertinggi yang akan "terkena". Misalnya, saat Anda menambahkan dua composable
Buttonyang tumpang-tindih keBox, hanya composable yang digambar di atas yang menerima peristiwa pointer. Secara teoritis, Anda dapat mengganti perilaku ini dengan membuat implementasiPointerInputModifierNodeAnda sendiri dan menetapkansharePointerInputWithSiblingske true. - Peristiwa lebih lanjut untuk pointer yang sama dikirim ke rantai composable yang sama, dan mengalir sesuai dengan logika penyebaran peristiwa. Sistem tidak melakukan hit-testing lagi untuk pointer ini. Artinya, setiap composable dalam rantai menerima semua peristiwa untuk pointer tersebut, meskipun peristiwa tersebut terjadi di luar batas composable tersebut. Composable yang tidak ada dalam rantai tidak akan pernah menerima peristiwa pointer, bahkan saat pointer berada di dalam batasnya.
Peristiwa pengarahan kursor, yang dipicu oleh pengarahan kursor mouse atau stilus, merupakan pengecualian untuk aturan yang ditentukan di sini. Peristiwa pengarahan kursor dikirim ke composable mana pun yang terkena. Jadi, saat pengguna mengarahkan kursor dari batas satu composable ke composable berikutnya, peristiwa akan dikirim ke composable baru, bukan ke composable pertama.
Konsumsi peristiwa
Jika lebih dari satu composable memiliki pengendali gestur yang ditetapkan, pengendali tersebut tidak boleh bertentangan. Misalnya, lihat UI ini:
Saat pengguna mengetuk tombol bookmark, lambda onClick tombol akan menangani gestur
tersebut. Saat pengguna mengetuk bagian lain dari item daftar, ListItem
akan menangani gestur tersebut dan membuka artikel. Dalam hal input pointer,
Tombol harus menggunakan peristiwa ini, sehingga induknya tahu untuk tidak
bereaksi lagi terhadapnya. Gestur yang disertakan dalam komponen siap pakai dan
pengubah gestur umum mencakup perilaku penggunaan ini, tetapi jika Anda
menulis gestur kustom sendiri, Anda harus menggunakan peristiwa secara manual. Anda dapat melakukannya
dengan metode PointerInputChange.consume:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Menggunakan peristiwa tidak menghentikan penyebaran peristiwa ke composable lain. Sebagai gantinya, composable harus secara eksplisit mengabaikan peristiwa yang digunakan. Saat menulis gestur kustom, Anda harus memeriksa apakah peristiwa sudah digunakan oleh elemen lain:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
Penyebaran peristiwa
Seperti yang disebutkan sebelumnya, perubahan pointer diteruskan ke setiap composable yang dikenainya.
Namun, jika ada lebih dari satu composable, dalam urutan apa peristiwa
disebarkan? Jika Anda mengambil contoh dari bagian terakhir, UI ini diterjemahkan ke
hierarki UI berikut, dengan hanya ListItem dan Button yang merespons
peristiwa pointer:
Peristiwa pointer mengalir melalui setiap composable ini sebanyak tiga kali, selama tiga "pass":
- Dalam Pass awal, peristiwa mengalir dari bagian atas hierarki UI ke
bawah. Alur ini memungkinkan induk menangkap peristiwa sebelum turunan dapat
menggunakannya. Misalnya, tooltip perlu mencegat
tekan lama, bukan meneruskannya ke turunannya. Dalam contoh
kita,
ListItemmenerima peristiwa sebelumButton. - Dalam Main pass, peristiwa mengalir dari node daun hierarki UI hingga ke
root hierarki UI. Fase ini adalah tempat Anda biasanya menggunakan gestur, dan merupakan
pass default saat memproses peristiwa. Menangani gestur dalam penerusan ini
berarti bahwa node daun lebih diprioritaskan daripada induknya, yang merupakan
perilaku paling logis untuk sebagian besar gestur. Dalam contoh kita,
Buttonmenerima peristiwa sebelumListItem. - Dalam Final pass, peristiwa mengalir sekali lagi dari bagian atas hierarki UI ke node daun. Alur ini memungkinkan elemen yang lebih tinggi dalam stack untuk merespons penggunaan peristiwa oleh induknya. Misalnya, tombol akan menghapus indikasi riaknya saat penekanan berubah menjadi penyeretan induk yang dapat di-scroll.
Secara visual, alur peristiwa dapat direpresentasikan sebagai berikut:
Setelah perubahan input digunakan, informasi ini akan diteruskan dari titik tersebut dalam alur dan seterusnya:
Dalam kode, Anda dapat menentukan penerusan yang Anda minati:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
Dalam cuplikan kode ini, peristiwa yang sama persis ditampilkan oleh setiap panggilan metode await ini, meskipun data tentang konsumsi mungkin telah berubah.
Menguji gestur
Dalam metode pengujian, Anda dapat mengirim peristiwa pointer secara manual menggunakan
metode performTouchInput. Hal ini memungkinkan Anda melakukan gestur penuh tingkat lebih tinggi (seperti mencubit atau mengklik lama) atau gestur tingkat rendah (seperti memindahkan kursor dengan jumlah piksel tertentu):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Lihat dokumentasi performTouchInput untuk contoh lainnya.
Pelajari lebih lanjut
Anda dapat mempelajari gestur di Jetpack Compose lebih lanjut dari referensi berikut:
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Aksesibilitas di Compose
- Scroll
- Ketuk dan tekan