Ada beberapa istilah dan konsep yang penting untuk dipahami saat menangani penanganan gestur dalam aplikasi. Halaman ini menjelaskan istilah pointer, peristiwa pointer, dan gestur, serta memperkenalkan berbagai tingkat abstraksi untuk gestur. Model ini juga membahas lebih dalam tentang konsumsi dan propagasi peristiwa.
Definisi
Untuk memahami berbagai konsep di halaman ini, Anda perlu memahami beberapa terminologi yang digunakan:
- Pointer: Objek fisik yang dapat digunakan untuk berinteraksi dengan aplikasi.
Untuk perangkat seluler, pointer yang paling umum adalah jari berinteraksi dengan
layar sentuh. Atau, Anda dapat menggunakan stilus untuk mengganti jari Anda.
Untuk perangkat layar besar, Anda dapat menggunakan mouse atau trackpad untuk berinteraksi secara tidak langsung dengan
layar. Perangkat input harus dapat "menunjuk" sebuah 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 tingkat rendah dari satu atau beberapa pointer
dengan aplikasi pada waktu tertentu. Setiap interaksi pointer, seperti meletakkan
jari pada layar atau menarik mouse, akan memicu suatu peristiwa. Di
Compose, semua informasi yang relevan untuk peristiwa tersebut dimuat dalam
class
PointerEvent
. - Gestur: Urutan peristiwa pointer yang dapat ditafsirkan sebagai satu tindakan. Misalnya, gestur ketuk dapat dianggap sebagai urutan peristiwa turun yang diikuti dengan peristiwa ke atas. 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
.
Biasanya, buat di tingkat abstraksi tertinggi yang menawarkan
fungsi yang Anda butuhkan. Dengan cara ini, Anda akan mendapatkan manfaat dari praktik terbaik yang disertakan dalam lapisan tersebut. 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 bekerja secara otomatis.
Selain penanganan gestur internal, banyak komponen juga memerlukan pemanggil untuk
menangani gestur. Misalnya, Button
otomatis mendeteksi ketukan
dan memicu peristiwa klik. Anda meneruskan lambda onClick
ke Button
untuk
merespons 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
menyertakan dukungan unik untuk fokus dan aksesibilitas, serta
teruji 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 aksesibilitas di Compose lebih lanjut, lihat Aksesibilitas di Compose.
Menambahkan gestur tertentu ke composable arbitrer dengan pengubah
Anda dapat menerapkan pengubah gestur ke composable arbitrer mana pun untuk membuat composable tersebut 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:
- Tangani ketukan dan penekanan dengan pengubah
clickable
,combinedClickable
,selectable
,toggleable
, dantriStateToggleable
. - Tangani scroll dengan pengubah
horizontalScroll
,verticalScroll
, dan yang lebih umumscrollable
. - Menangani penarikan dengan pengubah
draggable
danswipeable
. - Tangani gestur multi-kontrol seperti menggeser, memutar, dan memperbesar/memperkecil, dengan
pengubah
transformable
.
Sebagai aturan, lebih memilih pengubah gestur unik 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 setiap gestur diimplementasikan dengan pengubah gestur unik. Misalnya, Anda tidak dapat menggunakan pengubah untuk bereaksi terhadap tarik setelah menekan lama, klik kontrol, atau mengetuk dengan tiga jari. Sebagai gantinya, Anda dapat menulis pengendali gestur
Anda sendiri untuk mengidentifikasi gestur kustom ini. Anda dapat membuat pengendali gestur dengan
pengubah pointerInput
, yang memberi Anda akses ke peristiwa
pointer mentah.
Kode berikut memantau 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 cuplikan ini dipisahkan, komponen intinya adalah:
- Pengubah
pointerInput
. Anda meneruskan satu atau beberapa kunci. Saat nilai salah satu kunci tersebut berubah, lambda konten pengubah akan dijalankan kembali. Sampel meneruskan filter opsional ke composable. Jika nilai filter tersebut berubah, pengendali peristiwa pointer harus dijalankan kembali untuk memastikan peristiwa yang tepat dicatat ke dalam log. awaitPointerEventScope
membuat cakupan coroutine yang dapat digunakan untuk menunggu peristiwa pointer.awaitPointerEvent
menangguhkan coroutine hingga peristiwa pointer berikutnya terjadi.
Meskipun memproses peristiwa input mentah sangat efektif, menulis gestur kustom berdasarkan data mentah ini juga cukup rumit. Untuk menyederhanakan pembuatan gestur kustom, banyak metode utilitas tersedia.
Mendeteksi gestur penuh
Daripada menangani peristiwa pointer mentah, Anda dapat memproses gestur tertentu
agar terjadi dan merespons dengan tepat. AwaitPointerEventScope
menyediakan
metode untuk memproses:
- Tekan, ketuk, ketuk dua kali, dan tekan lama:
detectTapGestures
- Menarik:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
, dandetectDragGesturesAfterLongPress
- Transformasi:
detectTransformGestures
Ini adalah pendeteksi tingkat atas, sehingga Anda tidak dapat menambahkan beberapa pendeteksi dalam satu
pengubah pointerInput
. Cuplikan berikut hanya mendeteksi ketukan, bukan
tarik:
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 pendeteksi
kedua tidak pernah dijangkau. 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 definisi, gestur dimulai dengan peristiwa pointer down. Anda dapat menggunakan metode helper awaitEachGesture
, bukan loop while(true)
yang meneruskan setiap peristiwa mentah. Metode awaitEachGesture
akan memulai ulang
blok penampung saat semua pointer telah dicabut, yang menunjukkan gestur
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—hal ini hanya
perlu mengetahui kapan pointer memasuki atau keluar dari batasnya.
Menunggu peristiwa atau sub-gestur tertentu
Ada serangkaian metode yang membantu mengidentifikasi bagian-bagian umum dari {i>gesture <i}:
- Menangguhkan hingga pointer turun dengan
awaitFirstDown
, atau tunggu semua pointer naik denganwaitForUpOrCancellation
. - Buat pemroses tarik tingkat rendah menggunakan
awaitTouchSlopOrCancellation
danawaitDragOrCancellation
. Pengendali gestur terlebih dahulu ditangguhkan hingga pointer mencapai touch slop, lalu ditangguhkan hingga peristiwa tarik pertama muncul. Jika Anda hanya tertarik untuk menarik di sepanjang sumbu tunggal, gunakanawaitHorizontalTouchSlopOrCancellation
plusawaitHorizontalDragOrCancellation
, atauawaitVerticalTouchSlopOrCancellation
plusawaitVerticalDragOrCancellation
. - Menangguhkan hingga tekanan lama terjadi pada
awaitLongPressOrCancellation
. - Gunakan metode
drag
untuk terus memproses peristiwa tarik, atauhorizontalDrag
atauverticalDrag
untuk memproses peristiwa tarik pada satu sumbu.
Menerapkan penghitungan untuk peristiwa multi-sentuh
Saat pengguna melakukan gestur multi-kontrol menggunakan lebih dari satu pointer,
sulit untuk memahami transformasi yang diperlukan berdasarkan nilai mentah.
Jika pengubah transformable
atau metode detectTransformGestures
tidak memberikan kontrol terperinci yang cukup untuk kasus penggunaan, Anda dapat
memproses peristiwa mentah dan menerapkan penghitungannya pada peristiwa tersebut. Metode bantuan ini
adalah calculateCentroid
, calculateCentroidSize
,
calculatePan
, calculateRotation
, dan calculateZoom
.
Pengiriman peristiwa dan hit-test
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 memulai hit-testing composable yang "memenuhi syarat". Composable dianggap memenuhi syarat jika memiliki kemampuan penanganan input pointer. Hit-test mengalir dari bagian atas hierarki UI ke bawah. Composable adalah "hit" jika peristiwa pointer terjadi dalam batas composable tersebut. Proses ini menghasilkan rangkaian composable yang mencapai hit-test positif.
- Secara default, jika ada beberapa composable yang memenuhi syarat pada tingkat hierarki yang sama, hanya composable dengan indeks z tertinggi yang akan menjadi "hit". Misalnya, saat Anda menambahkan dua composable
Button
yang tumpang-tindih keBox
, hanya yang digambar di bagian atas yang akan menerima peristiwa pointer. Secara teoritis, Anda dapat mengganti perilaku ini dengan membuat penerapanPointerInputModifierNode
Anda sendiri dan menetapkansharePointerInputWithSiblings
ke benar (true). - Peristiwa lebih lanjut untuk pointer yang sama dikirim ke rantai composable yang sama, dan mengalir sesuai dengan logika propagasi peristiwa. Sistem tidak melakukan lagi hit-testing 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, meskipun pointer berada di dalam batasnya.
Peristiwa pengarahan kursor, yang dipicu oleh kursor mouse atau stilus, merupakan pengecualian terhadap aturan yang ditentukan di sini. Peristiwa pengarahan kursor dikirim ke composable mana pun yang diklik. Jadi, saat pengguna mengarahkan pointer dari batas satu composable ke composable berikutnya, bukan mengirim peristiwa ke composable pertama tersebut, peristiwa akan dikirim ke composable baru.
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, agar induknya tahu untuk tidak
bereaksi lagi. Gestur yang disertakan dalam komponen siap pakai dan
pengubah gestur umum menyertakan perilaku konsumsi ini, tetapi jika Anda
menulis gestur kustom sendiri, Anda harus menggunakan peristiwa secara manual. Anda melakukannya
dengan metode PointerInputChange.consume
:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
Memakai peristiwa tidak akan menghentikan penerapan peristiwa ke composable lain. Sebagai gantinya, composable harus secara eksplisit mengabaikan peristiwa yang digunakan. Saat menulis gestur kustom, Anda harus memeriksa apakah suatu 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 } } } }
Penerapan peristiwa
Seperti yang disebutkan sebelumnya, perubahan pointer diteruskan ke setiap composable yang ditemukan.
Namun, jika ada lebih dari satu composable, dalam urutan bagaimana peristiwa
diterapkan? Jika Anda mengambil contoh dari bagian terakhir, UI ini akan diterjemahkan
ke hierarki UI berikut, dengan hanya ListItem
dan Button
yang merespons
peristiwa pointer:
Peristiwa pointer mengalir melalui setiap composable ini tiga kali, selama tiga "terus":
- Pada Tahap awal, peristiwa mengalir dari bagian atas hierarki UI ke
bawah. Alur ini memungkinkan induk untuk menangkap peristiwa sebelum anak dapat menggunakannya. Misalnya, tooltip perlu menangkap
tekan lama, bukan meneruskannya ke turunannya. Dalam contoh kita,
ListItem
menerima peristiwa sebelumButton
. - Di Jalur utama, peristiwa mengalir dari node daun hierarki UI hingga
root hierarki UI. Fase ini adalah saat Anda biasanya menggunakan gestur, dan merupakan
proses default saat memproses peristiwa. Menangani gestur dalam penerusan ini
berarti bahwa node daun lebih diutamakan daripada induknya, yang merupakan
perilaku paling logis untuk sebagian besar gestur. Dalam contoh kita,
Button
menerima peristiwa sebelumListItem
. - Di 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 pemakaian peristiwa oleh induknya. Misalnya, tombol menghapus indikasi ripple saat penekanan berubah menjadi tarik dari induknya yang dapat di-scroll.
Secara visual, alur peristiwa dapat direpresentasikan sebagai berikut:
Setelah perubahan input digunakan, informasi ini diteruskan dari titik tersebut dalam alur dan seterusnya:
Dalam kode, Anda dapat menentukan kartu 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 identik yang sama ditampilkan oleh setiap panggilan metode tunggu 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 cubit atau klik lama) atau gestur tingkat rendah (seperti
menggerakkan 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