Memahami gestur

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:

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:

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

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 ke Box, hanya yang digambar di atas menerima kejadian pointer. Anda dapat secara teoretis dapat mengganti perilaku ini dengan membuat PointerInputModifierNode Anda sendiri dan menetapkan sharePointerInputWithSiblings 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:

Item daftar dengan Gambar, Kolom dengan dua teks, dan Tombol.

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:

Struktur pohon. Lapisan atas adalah ListItem, lapisan kedua memiliki Image, Column, dan Button, dan Column dibagi menjadi dua Text. ListItem dan Button ditandai.

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 sebelum Button.
  • 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 sebelum ListItem.
  • 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: