Ada beberapa istilah dan konsep yang penting untuk dipahami saat mengerjakan penanganan {i>gesture <i}dalam aplikasi. Halaman ini menjelaskan persyaratan pointer, peristiwa pointer, dan gestur, serta memperkenalkan abstraksi yang berbeda untuk {i>gesture.<i} Model ini juga menyelami lebih dalam konsumsi peristiwa dan propagasi.
Definisi
Untuk memahami berbagai konsep pada halaman ini, Anda perlu memahami beberapa dari terminologi yang digunakan:
- Pointer: Objek fisik yang dapat digunakan untuk berinteraksi dengan aplikasi.
Untuk perangkat seluler, pointer yang paling umum adalah jari yang berinteraksi dengan
layar sentuh. Atau, Anda dapat menggunakan stilus sebagai pengganti jari Anda.
Untuk perangkat layar besar, Anda dapat menggunakan mouse atau trackpad untuk berinteraksi secara tidak langsung
layar. Perangkat input harus dapat "mengarahkan" di sebuah koordinat agar
dianggap sebagai {i>pointer<i}, jadi {i>keyboard<i}, misalnya, tidak dapat dianggap
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 menempatkan
jari pada layar atau menyeret {i>mouse<i}, akan memicu suatu peristiwa. Di beberapa
Compose, semua informasi yang relevan untuk peristiwa tersebut dimuat dalam
Class
PointerEvent
. - Gestur: Urutan peristiwa pointer yang dapat ditafsirkan sebagai satu tindakan. Misalnya, {i>gesture <i}ketuk bisa dianggap sebagai urutan dari bawah diikuti oleh peristiwa naik. Ada banyak {i>gesture <i}umum yang digunakan oleh banyak seperti ketuk, tarik, atau ubah. Namun, Anda juga dapat membuat aplikasi kustom {i>gesture-<i}nya jika diperlukan.
Tingkat abstraksi yang berbeda
Jetpack Compose menyediakan berbagai level abstraksi untuk menangani gestur.
Di tingkat teratas adalah dukungan komponen. Composable seperti Button
menyertakan dukungan gestur secara otomatis. Untuk menambahkan dukungan gestur ke
Anda dapat menambahkan pengubah gestur seperti clickable
ke arbitrer
composable. Terakhir, jika memerlukan {i>gesture <i}khusus, Anda dapat menggunakan
Pengubah pointerInput
.
Biasanya, bangun di tingkat abstraksi tertinggi yang menawarkan
fungsionalitas yang Anda butuhkan. Dengan cara ini, Anda akan mendapat 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 mentah
implementasi pointerInput
.
Dukungan komponen
Banyak komponen siap pakai di Compose menyertakan semacam gestur internal
penanganannya. Misalnya, LazyColumn
merespons gestur tarik dengan
men-scroll kontennya, Button
menampilkan riak saat Anda menekannya,
dan komponen SwipeToDismiss
berisi logika menggeser untuk menutup
. Jenis penanganan gestur ini bekerja secara otomatis.
Di samping penanganan gestur internal, banyak komponen juga mengharuskan pemanggil untuk
menangani {i>gesture-<i}nya. 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 {i>gesture <i}yang disertakan dalam komponen, karena
menyertakan dukungan siap pakai untuk fokus dan aksesibilitas, dan itu
teruji dengan baik. Misalnya, Button
ditandai dengan cara khusus sehingga
layanan aksesibilitas mendeskripsikannya
dengan benar sebagai sebuah tombol, bukan
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 Tulis.
Menambahkan gestur tertentu ke composable arbitrer dengan pengubah
Anda dapat menerapkan pengubah gestur ke composable arbitrer untuk membuat
composable mendengarkan 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
clickable
,combinedClickable
,selectable
,toggleable
, dan PengubahtriStateToggleable
. - Menangani scroll dengan
horizontalScroll
,verticalScroll
, dan pengubahscrollable
yang lebih umum. - Menangani penarikan dengan
draggable
danswipeable
pengubah. - Menangani gestur multi-kontrol seperti menggeser, memutar, dan memperbesar/memperkecil, dengan
pengubah
transformable
.
Sebagai aturan, pilih pengubah gestur siap pakai daripada penanganan gestur kustom.
Pengubah menambahkan lebih banyak fungsi di atas penanganan peristiwa pointer murni.
Misalnya, pengubah clickable
tidak hanya menambahkan deteksi penekanan dan
ketukan, tetapi juga menambahkan informasi semantik, indikasi visual pada interaksi,
mengarahkan kursor, fokus, dan
dukungan keyboard. Anda dapat memeriksa kode sumber
dari clickable
untuk melihat bagaimana fungsi
sedang ditambahkan.
Menambahkan gestur kustom ke composable arbitrer dengan pengubah pointerInput
Tidak semua gestur diterapkan dengan pengubah gestur siap pakai. Sebagai
misalnya, Anda tidak dapat menggunakan pengubah untuk bereaksi terhadap tarikan setelah menekan lama,
kontrol-klik, atau ketuk dengan tiga jari. Sebagai gantinya, Anda bisa menulis gestur Anda sendiri
untuk mengidentifikasi gestur kustom ini. Anda dapat membuat pengendali gestur dengan
pengubah pointerInput
, yang memberi Anda akses ke pointer mentah
peristiwa.
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 memecah cuplikan ini, komponen intinya adalah:
- Pengubah
pointerInput
. Anda meneruskan satu atau beberapa kunci. Jika dari salah satu tombol tersebut berubah, lambda konten pengubah adalah dijalankan kembali. Contoh ini meneruskan filter opsional ke composable. Jika nilai filter itu berubah, pengendali peristiwa pointer harus dieksekusi kembali untuk memastikan peristiwa yang tepat dicatat. awaitPointerEventScope
membuat cakupan coroutine yang dapat digunakan untuk menunggu peristiwa pointer.awaitPointerEvent
menangguhkan coroutine hingga peristiwa pointer berikutnya apa yang terjadi.
Meskipun memproses peristiwa input mentah sangat ampuh, menulisnya juga rumit {i>gesture <i}khusus berdasarkan data mentah ini. Untuk menyederhanakan pembuatan {i>gesture, <i}banyak metode utilitas yang tersedia.
Mendeteksi gestur lengkap
Daripada menangani peristiwa pointer mentah, Anda dapat memproses gestur tertentu
terjadi dan menanggapinya dengan tepat. AwaitPointerEventScope
menyediakan
metode untuk memproses:
- Tekan, ketuk, ketuk dua kali, dan tekan lama:
detectTapGestures
- Tarik:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
, dandetectDragGesturesAfterLongPress
- Transformasi:
detectTransformGestures
Pendeteksi tersebut merupakan tingkat teratas, sehingga Anda tidak dapat menambahkan beberapa pendeteksi dalam satu pendeteksi.
Pengubah pointerInput
. Cuplikan berikut hanya mendeteksi ketukan, bukan ketukan
menarik:
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 yang kedua
detektor 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 definisi, gestur dimulai dengan peristiwa pointer ke bawah. Anda dapat menggunakan
Metode helper awaitEachGesture
, bukan loop while(true)
yang
melewati setiap peristiwa mentah. Metode awaitEachGesture
akan memulai ulang
yang berisi blok ketika semua pointer telah diangkat, yang menunjukkan bahwa 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
menanggapi kejadian pointer tanpa mengidentifikasi {i>gestures.<i} Contohnya adalah
hoverable
, yang tidak merespons peristiwa pointer ke bawah atau ke atas, tetapi hanya
perlu mengetahui kapan pointer memasuki atau keluar dari batasnya.
Menunggu peristiwa atau sub-gestur tertentu
Ada seperangkat metode yang membantu mengidentifikasi bagian-bagian umum dari {i>gesture<i}:
- Menangguhkan hingga pointer turun dengan
awaitFirstDown
, atau menunggu semua pointer untuk naik denganwaitForUpOrCancellation
. - Membuat pemroses tarik tingkat rendah menggunakan
awaitTouchSlopOrCancellation
danawaitDragOrCancellation
. Pengendali gestur pertama kali ditangguhkan hingga pointer mencapai slop sentuh, lalu ditangguhkan hingga peristiwa tarik pertama diterapkan. Jika Anda hanya tertarik dengan {i>drag<i} di sepanjang satu sumbu, gunakanawaitHorizontalTouchSlopOrCancellation
ke atasawaitHorizontalDragOrCancellation
, atauawaitVerticalTouchSlopOrCancellation
ke atasawaitVerticalDragOrCancellation
sebagai gantinya. - Tangguhkan hingga penekanan lama terjadi dengan
awaitLongPressOrCancellation
. - Gunakan metode
drag
untuk terus memproses peristiwa tarik, atauhorizontalDrag
atauverticalDrag
untuk memproses peristiwa tarik di satu tempat .
Menerapkan penghitungan untuk peristiwa multi-sentuh
Saat pengguna melakukan {i>multi-touch<i}
menggunakan lebih dari satu pointer,
sulit untuk memahami transformasi yang
dibutuhkan berdasarkan nilai mentah.
Jika pengubah transformable
atau detectTransformGestures
tidak memberikan kontrol yang cukup terperinci untuk kasus penggunaan Anda, Anda bisa
mendengarkan peristiwa mentah dan menerapkan
kalkulasi 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
. Acara
pengiriman berfungsi sebagai berikut:
- Peristiwa pointer dikirim ke hierarki composable. Suatu saat ketika pointer baru memicu peristiwa pointer pertamanya, sistem akan memulai hit-testing "memenuhi syarat" composable. Composable dianggap memenuhi syarat jika memiliki kemampuan penanganan input pointer. Alur hit-testing dari bagian atas UI pohon ke bawah. Composable adalah "hit" saat peristiwa pointer terjadi dalam batas-batas composable tersebut. Proses ini menghasilkan rantai composable yang hit-test secara positif.
- Secara default, jika ada beberapa composable yang memenuhi syarat di tingkat yang sama
hierarki, hanya composable dengan indeks z tertinggi yang merupakan "hit". Sebagai
contoh, saat Anda menambahkan dua composable
Button
yang tumpang-tindih keBox
, hanya yang digambar di atas menerima kejadian pointer. Anda dapat secara teoretis dapat mengganti perilaku ini dengan membuatPointerInputModifierNode
Anda sendiri dan menetapkansharePointerInputWithSiblings
ke benar (true). - Kejadian lebih lanjut untuk pointer yang sama dikirim ke rantai yang sama dari composable, dan mengalir sesuai dengan logika propagasi peristiwa. Sistem tidak melakukan hit-test lagi untuk pointer ini. Ini berarti bahwa setiap yang dapat disusun dalam rantai menerima semua peristiwa untuk pointer tersebut, bahkan saat yang terjadi di luar batas composable tersebut. Composable yang tidak dalam rantai tidak pernah menerima peristiwa pointer, bahkan ketika pointer di dalam batasnya.
Peristiwa pengarahan kursor, yang dipicu oleh kursor mouse atau stilus, merupakan pengecualian untuk aturan yang didefinisikan di sini. Peristiwa pengarahan kursor dikirim ke composable apa pun yang dijangkaunya. Namun saat pengguna mengarahkan pointer dari batas satu composable ke composable berikutnya, bukan mengirim peristiwa ke composable pertama tersebut, peristiwa akan dikirim ke composable baru.
Pemakaian peristiwa
Jika lebih dari satu composable memiliki pengendali gestur yang ditetapkan untuknya, composable tersebut tidak boleh mengalami konflik. Misalnya, lihat UI ini:
Saat pengguna mengetuk tombol bookmark, lambda onClick
tombol akan menanganinya
{i>gesture <i}ini. Saat pengguna mengetuk bagian lain dari item daftar, ListItem
menangani {i>gesture <i}tersebut
dan menavigasi ke artikel. Dalam hal input pointer,
Tombol harus menggunakan peristiwa ini, agar induknya tahu untuk tidak
akan bereaksi lagi. {i>Gesture <i}yang termasuk dalam komponen {i>out-of-the-box<i} dan
pengubah {i>gesture <i}umum mencakup perilaku konsumsi ini, tetapi jika
menulis gestur kustom sendiri, Anda harus memakai peristiwa secara manual. Anda yang harus 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 propagasi peristiwa ke composable lain. J composable harus secara eksplisit mengabaikan peristiwa yang digunakan. Saat menulis gestur kustom, Anda harus memeriksa apakah suatu peristiwa sudah dipakai oleh :
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 } } } }
Propagasi peristiwa
Seperti yang disebutkan sebelumnya, perubahan pointer diteruskan ke setiap composable yang dicapai.
Namun, jika ada lebih dari satu composable tersebut, dalam urutan bagaimana peristiwa tersebut
terapkan? Jika Anda mengambil contoh dari bagian terakhir, UI ini akan diterjemahkan menjadi
hierarki UI berikut, dengan hanya ListItem
dan Button
yang merespons
peristiwa pointer:
Peristiwa pointer mengalir melalui setiap composable ini tiga kali, selama tiga "lewat":
- Pada Penerusan awal, peristiwa mengalir dari bagian atas hierarki UI ke
ke bawah. Alur ini memungkinkan induk mencegat suatu peristiwa sebelum anaknya dapat
akan menggunakannya. Misalnya, tooltip harus mencegat peristiwa
menekan lama, bukan meneruskannya ke turunan. Di
misalnya,
ListItem
menerima peristiwa sebelumButton
. - Di Main pass, peristiwa mengalir dari node daun hierarki UI ke
root hierarki UI. Fase ini adalah di mana Anda biasanya
menggunakan {i>gesture, <i}dan
{i>pass default<i} saat memproses peristiwa. Menangani gestur dalam penerusan ini
berarti bahwa simpul {i>leaf<i} lebih diprioritaskan
daripada induknya, yang merupakan
perilaku yang paling logis untuk
sebagian besar {i>gesture<i}. Dalam contoh kita,
Button
menerima peristiwa sebelumListItem
. - Di Penerusan akhir, peristiwa mengalir sekali lagi dari bagian atas UI pohon ke simpul daun. Alur ini memungkinkan elemen yang lebih tinggi dalam tumpukan untuk merespons pemakaian peristiwa oleh induknya. Misalnya, sebuah tombol menghapus indikasi ripple-nya saat penekanan berubah menjadi tarik untuk induk yang dapat di-scroll.
Secara visual, alur peristiwa dapat direpresentasikan sebagai berikut:
Setelah perubahan {i>input<i} digunakan, informasi ini akan diteruskan dari sana 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 dikembalikan oleh masing-masing panggilan metode ini menunggu, meskipun data tentang pemakaian mungkin ubah.
Menguji gestur
Dalam metode pengujian, Anda bisa mengirim kejadian pointer secara manual menggunakan
Metode performTouchInput
. Hal ini memungkinkan Anda menjalankan tugas
gestur penuh (seperti mencubit atau klik lama) atau gestur level rendah (seperti
menggerakkan kursor sebesar jumlah {i>pixel<i} 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 berikut ini referensi:
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Aksesibilitas di Compose
- Scroll
- Ketuk dan tekan