Fitur stilus lanjutan

Android dan ChromeOS menyediakan berbagai API untuk membantu Anda membangun aplikasi yang menawarkan pengalaman stilus yang luar biasa kepada pengguna. Tujuan Eksposur class MotionEvent informasi tentang interaksi {i>stylus<i} dengan layar, termasuk tekanan {i>stylus<i}, orientasi, kemiringan, pengarahan kursor, dan deteksi telapak tangan. Grafis dan gerakan latensi rendah library prediksi meningkatkan rendering stilus di layar untuk memberikan pengalaman yang alami, seperti pena dan kertas.

MotionEvent

Class MotionEvent mewakili interaksi input pengguna seperti posisi dan pergerakan pointer sentuh di layar. Untuk input stilus, MotionEvent juga menampilkan data tekanan, orientasi, kemiringan, dan pengarahan kursor.

Data peristiwa

Untuk mengakses data MotionEvent, tambahkan pengubah pointerInput ke komponen:

@Composable
fun Greeting() {
    Text(
        text = "Hello, Android!", textAlign = TextAlign.Center, style = TextStyle(fontSize = 5.em),
        modifier = Modifier
            .pointerInput(Unit) {
                awaitEachGesture {
                    while (true) {
                        val event = awaitPointerEvent()
                        event.changes.forEach { println(it) }
                    }
                }
            },
    )
}

Objek MotionEvent memberikan data yang terkait dengan aspek UI berikut acara:

  • Tindakan: Interaksi fisik dengan perangkat—menyentuh layar, menggerakkan pointer ke permukaan layar, mengarahkan kursor ke layar permukaan
  • Pointer: ID objek yang berinteraksi dengan layar—jari, stilus, mouse
  • Sumbu: Jenis data—koordinat x dan y, tekanan, kemiringan, orientasi, dan arahkan kursor (jarak)

Tindakan

Untuk menerapkan dukungan stilus, Anda harus memahami tindakan yang dilakukan pengguna berperforma tinggi.

MotionEvent menyediakan berbagai konstanta ACTION yang menentukan gerakan peristiwa. Tindakan paling penting untuk stilus mencakup hal-hal berikut:

Tindakan Deskripsi
ACTION_DOWN
ACTION_POINTER_DOWN
Pointer telah melakukan kontak dengan layar.
ACTION_MOVE Pointer bergerak di layar.
ACTION_UP
ACTION_POINTER_UP
Pointer tidak lagi terhubung dengan layar
ACTION_CANCEL Saat kumpulan gerakan sebelumnya atau saat ini harus dibatalkan.

Aplikasi Anda dapat melakukan tugas seperti memulai goresan baru saat ACTION_DOWN terjadi, menggambar goresan dengan ACTION_MOVE, dan menyelesaikan goresan ketika ACTION_UP dipicu.

Kumpulan tindakan MotionEvent dari ACTION_DOWN hingga ACTION_UP untuk suatu pointer disebut himpunan gerakan.

Pointer

Sebagian besar layar multi-sentuh: sistem menetapkan pointer untuk setiap jari, stilus, mouse, atau objek pointer lainnya yang berinteraksi dengan layar. Pointer Index memungkinkan Anda mendapatkan informasi sumbu untuk pointer tertentu, seperti posisi jari pertama yang menyentuh layar atau yang kedua.

Indeks pointer berkisar antara nol hingga jumlah pointer yang ditampilkan oleh MotionEvent#pointerCount() kurang 1.

Nilai sumbu pointer dapat diakses dengan metode getAxisValue(axis, pointerIndex). Jika indeks pointer dihilangkan, sistem akan mengembalikan nilai untuk yang pertama pointer, pointer nol (0).

Objek MotionEvent berisi informasi tentang jenis pointer yang digunakan. Anda bisa mendapatkan tipe pointer dengan melakukan iterasi melalui indeks pointer dan memanggil tindakan getToolType(pointerIndex) .

Untuk mempelajari pointer lebih lanjut, lihat Menangani multi-sentuh gestur.

Input stilus

Anda dapat memfilter input stilus dengan TOOL_TYPE_STYLUS:

val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)

Stilus juga dapat melaporkan bahwa stilus digunakan sebagai penghapus dengan TOOL_TYPE_ERASER:

val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)

Data sumbu stilus

ACTION_DOWN dan ACTION_MOVE menyediakan data sumbu tentang stilus, yaitu x dan koordinat y, tekanan, orientasi, kemiringan, dan pengarahan kursor.

Untuk mengaktifkan akses ke data ini, MotionEvent API menyediakan getAxisValue(int), dengan parameternya adalah salah satu ID sumbu berikut:

Sumbu Nilai yang ditampilkan getAxisValue()
AXIS_X Koordinat X dari suatu peristiwa gerakan.
AXIS_Y Koordinat Y dari suatu peristiwa gerakan.
AXIS_PRESSURE Untuk layar sentuh atau touchpad, tekanan diaplikasikan oleh jari, stilus, atau pointer lainnya. Untuk mouse atau trackball, 1 jika tombol utama ditekan, 0 jika sebaliknya.
AXIS_ORIENTATION Untuk layar sentuh atau touchpad, orientasi jari, stilus, atau pointer lainnya terkait dengan bidang vertikal perangkat.
AXIS_TILT Sudut kemiringan stilus dalam radian.
AXIS_DISTANCE Jarak stilus dari layar.

Misalnya, MotionEvent.getAxisValue(AXIS_X) menampilkan koordinat x untuk pointer pertama.

Lihat juga Menangani multi-sentuh gestur.

Posisi

Anda dapat mengambil koordinat x dan y pointer dengan panggilan berikut:

Gambar stilus di layar dengan koordinat x dan y yang dipetakan.
Gambar 1. Koordinat X dan y dari pointer stilus.

Tekanan

Anda dapat mengambil tekanan {i>pointer<i} dengan MotionEvent#getAxisValue(AXIS_PRESSURE), atau untuk pointer pertama, MotionEvent#getPressure().

Nilai tekanan untuk layar sentuh atau touchpad adalah nilai antara 0 (tidak tekanan) dan 1, namun nilai yang lebih tinggi dapat dikembalikan tergantung pada layar untuk kalibrasi.

Goresan stilus yang merepresentasikan kontinum tekanan rendah hingga tinggi. Gaya goresan sempit dan lemah di sebelah kiri, menunjukkan tekanan rendah. Goresan menjadi lebih lebar dan lebih gelap dari kiri ke kanan hingga menjadi paling luas dan paling gelap di ujung kanan, menunjukkan tekanan tertinggi.
Gambar 2. Representasi tekanan—tekanan rendah di kiri, tekanan tinggi di kanan.

Orientasi

Orientasi menunjukkan arah stilus yang ditunjuk.

Orientasi pointer dapat diambil menggunakan getAxisValue(AXIS_ORIENTATION) atau getOrientation() (untuk pointer pertama).

Untuk stilus, orientasi ditampilkan sebagai nilai radian antara 0 hingga pi (π) searah jarum jam atau 0 ke -pi berlawanan arah jarum jam.

Orientasi memungkinkan Anda menerapkan kuas sungguhan. Misalnya, jika {i>stylus <i}mewakili kuas datar, lebar kuas datar tergantung pada orientasi stilus.

Gambar 3. Stilus menunjuk ke kiri sekitar minus 0,57 radian.

Kemiringan

Kemiringan mengukur kemiringan stilus relatif terhadap layar.

Kemiringan menampilkan sudut positif stilus dalam radian, dengan nol adalah tegak lurus terhadap layar dan π/2 datar di permukaan.

Sudut kemiringan dapat diambil menggunakan getAxisValue(AXIS_TILT) (tanpa pintasan untuk pointer pertama).

Kemiringan dapat digunakan untuk mereproduksi alat di dunia nyata semirip mungkin, seperti meniru bayangan dengan pensil miring.

Stilus miring ke sekitar 40 derajat dari permukaan layar.
Gambar 4. Stilus miring sekitar 0,785 radian, atau 45 derajat dari tegak lurus.

Arahkan kursor

Jarak stilus dari layar dapat diperoleh dengan getAxisValue(AXIS_DISTANCE). Metode ini menampilkan nilai dari 0.0 (kontak dengan layar) ke nilai yang lebih tinggi saat stilus bergerak menjauh dari layar. Pengarahan kursor jarak antara layar dan ujung pena (titik) stilus bergantung pada produsen layar dan stilus. Karena implementasi dapat bervariasi, jangan mengandalkan nilai yang tepat untuk fungsi yang penting bagi aplikasi.

Stilus dalam keadaan melayang dapat digunakan untuk melihat pratinjau ukuran kuas atau menunjukkan bahwa akan dipilih.

Gambar 5. Stilus melayang di atas layar. Aplikasi bereaksi meskipun stilus tidak menyentuh permukaan layar.

Catatan: Compose menyediakan pengubah yang memengaruhi status interaktif elemen UI:

  • hoverable: Mengonfigurasi komponen agar dapat diarahkan menggunakan peristiwa masuk dan keluar pointer.
  • indication: Menggambar efek visual untuk komponen ini saat interaksi terjadi.

Penolakan telapak tangan, navigasi, dan input yang tidak diinginkan

Terkadang layar multi-sentuh dapat mencatat sentuhan yang tidak diinginkan, misalnya, ketika pengguna secara alami meletakkan tangannya di layar untuk penyangga saat menulis tangan. Penolakan telapak tangan adalah mekanisme yang mendeteksi perilaku ini dan memberi tahu Anda bahwa kumpulan MotionEvent terakhir harus dibatalkan.

Oleh karena itu, Anda harus menyimpan histori input pengguna agar sentuhan yang tidak diinginkan dapat dihapus dari layar dan input pengguna yang sah dapat dirender ulang.

ACTION_CANCEL dan FLAG_CANCELED

ACTION_CANCEL dan FLAG_CANCELED adalah keduanya dirancang untuk menginformasikan kepada Anda bahwa kumpulan MotionEvent sebelumnya harus dibatalkan dari ACTION_DOWN terakhir, sehingga Anda dapat, misalnya, mengurungkan {i>stroke<i} untuk aplikasi menggambar untuk pointer tertentu.

ACTION_CANCEL

Ditambahkan di Android 1.0 (API level 1)

ACTION_CANCEL menunjukkan kumpulan peristiwa gerakan sebelumnya yang harus dibatalkan.

ACTION_CANCEL dipicu saat salah satu dari hal berikut terdeteksi:

  • Gestur navigasi
  • Penolakan telapak tangan

Saat ACTION_CANCEL dipicu, Anda harus mengidentifikasi pointer aktif dengan getPointerId(getActionIndex()). Kemudian, hapus goresan yang dibuat dengan pointer tersebut dari histori input, dan render ulang scene.

FLAG_CANCELED

Ditambahkan di Android 13 (API level 33)

FLAG_CANCELED menunjukkan bahwa pointer naik merupakan sentuhan yang tidak disengaja oleh pengguna. Penandanya adalah biasanya disetel ketika pengguna secara tidak sengaja menyentuh layar, misalnya dengan menggenggam perangkat atau meletakkan telapak tangan di layar.

Anda mengakses nilai flag sebagai berikut:

val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED

Jika tanda ini disetel, Anda harus mengurungkan kumpulan MotionEvent terakhir, dari ACTION_DOWN dari pointer ini.

Seperti ACTION_CANCEL, pointer dapat ditemukan dengan getPointerId(actionIndex).

Gambar 6. Goresan stilus dan sentuhan telapak tangan membuat kumpulan MotionEvent. Sentuhan telapak tangan dibatalkan, dan layar dirender ulang.

Layar penuh, tata letak layar penuh, dan gestur navigasi

Jika aplikasi dalam mode layar penuh dan memiliki elemen yang dapat ditindaklanjuti di dekat tepi, seperti kanvas aplikasi menggambar atau pencatat, menggeser dari bagian bawah layar untuk menampilkan navigasi atau memindahkan aplikasi ke latar belakang dapat mengakibatkan sentuhan yang tidak diinginkan pada kanvas.

Gambar 7. Geser gestur untuk memindahkan aplikasi ke latar belakang.

Untuk mencegah gestur memicu sentuhan yang tidak diinginkan di aplikasi, Anda dapat melakukan keunggulan inset dan ACTION_CANCEL.

Lihat juga Penolakan, navigasi, dan input yang tidak diinginkan bagian.

Gunakan setSystemBarsBehavior() metode dan BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE pengguna WindowInsetsController untuk mencegah gestur navigasi menyebabkan peristiwa sentuh yang tidak diinginkan:

// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

Untuk mempelajari pengelolaan inset dan gestur lebih lanjut, lihat:

Latensi rendah

Latensi adalah waktu yang dibutuhkan oleh hardware, sistem, dan aplikasi untuk memproses dan merender input pengguna.

Latensi = pemrosesan input hardware dan OS + pemrosesan aplikasi + komposisi sistem

  • rendering hardware
Latensi menyebabkan goresan yang dirender tertinggal di belakang posisi stilus. Jeda antara goresan yang dirender dan posisi stilus mewakili latensi.
Gambar 8. Latensi menyebabkan goresan yang dirender tertinggal di belakang posisi stilus.

Sumber latensi

  • Mendaftarkan stilus dengan layar sentuh (hardware): Koneksi nirkabel awal saat stilus dan OS berkomunikasi untuk didaftarkan dan disinkronkan.
  • Frekuensi sampling sentuhan (hardware): Frekuensi layar sentuh per detik memeriksa apakah pointer menyentuh permukaan, berkisar antara 60 hingga 1000 Hz.
  • Pemrosesan input (aplikasi): Menerapkan warna, efek grafis, dan transformasi input pengguna.
  • Rendering grafis (OS + hardware): Pertukaran buffer, pemrosesan hardware.

Grafis latensi rendah

Library grafis latensi rendah Jetpack mengurangi waktu pemrosesan antara input pengguna dan rendering di layar.

Library mengurangi waktu pemrosesan dengan menghindari rendering multi-buffer dan memanfaatkan teknik rendering buffer depan, yang berarti menulis langsung ke layar.

Rendering buffer depan

Buffer depan adalah memori yang digunakan layar untuk rendering. Ini adalah yang terdekat aplikasi bisa menggambar langsung ke layar. Library latensi rendah memungkinkan aplikasi untuk merender langsung ke buffer depan. Hal ini meningkatkan performa dengan mencegah pertukaran buffer, yang dapat terjadi untuk rendering multi-buffer reguler atau rendering buffer ganda (kasus yang paling umum).

Aplikasi menulis ke buffer layar dan membaca dari buffer layar.
Gambar 9. Rendering buffer depan.
Aplikasi menulis ke multi-buffer yang ditukar dengan buffer layar. Aplikasi membaca dari buffer layar.
Gambar 10. Rendering multi-buffer.

Meskipun rendering buffer depan merupakan teknik yang baik untuk merender area kecil dari layar, tidak dirancang untuk digunakan untuk memuat ulang seluruh layar. Dengan rendering buffer depan, aplikasi sedang merender konten ke buffer layar sedang membaca. Akibatnya, ada kemungkinan rendering artefak atau tearing (lihat di bawah).

Library latensi rendah tersedia dari Android 10 (API level 29) dan yang lebih tinggi dan di perangkat ChromeOS yang menjalankan Android 10 (level API 29) dan yang lebih tinggi.

Dependensi

Library latensi rendah menyediakan komponen untuk rendering buffer depan terlepas dari implementasi layanan. Library ditambahkan sebagai dependensi dalam modul aplikasi File build.gradle:

dependencies {
    implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}

Callback GLFrontBufferRenderer

Library latensi rendah menyertakan GLFrontBufferRenderer.Callback , yang menentukan metode berikut:

Library latensi rendah tidak memiliki opini terkait jenis data yang Anda gunakan dengan GLFrontBufferRenderer.

Namun, library ini memproses data sebagai aliran dari ratusan titik data; dan kemudian, rancang data Anda untuk mengoptimalkan penggunaan dan alokasi memori.

Callback

Untuk mengaktifkan callback rendering, terapkan GLFrontBufferedRenderer.Callback dan menggantikan onDrawFrontBufferedLayer() dan onDrawDoubleBufferedLayer(). GLFrontBufferedRenderer menggunakan callback untuk merender data Anda seoptimal mungkin.

val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> {
   override fun onDrawFrontBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       param: DATA_TYPE
   ) {
       // OpenGL for front buffer, short, affecting small area of the screen.
   }
   override fun onDrawMultiDoubleBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       params: Collection<DATA_TYPE>
   ) {
       // OpenGL full scene rendering.
   }
}
Mendeklarasikan instance GLFrontBufferedRenderer

Siapkan GLFrontBufferedRenderer dengan menyediakan SurfaceView dan yang telah Anda buat sebelumnya. GLFrontBufferedRenderer mengoptimalkan rendering ke buffer depan dan ganda menggunakan callback Anda:

var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Rendering

Rendering buffer depan dimulai saat Anda memanggil renderFrontBufferedLayer() , yang memicu callback onDrawFrontBufferedLayer().

Rendering buffer ganda dilanjutkan saat Anda memanggil commit() , yang akan memicu callback onDrawMultiDoubleBufferedLayer().

Dalam contoh berikut, proses merender ke buffer depan (cepat rendering) saat pengguna mulai menggambar di layar (ACTION_DOWN) dan bergerak kursor di sekitar (ACTION_MOVE). Proses merender ke buffer ganda saat pointer meninggalkan permukaan layar (ACTION_UP).

Anda dapat menggunakan requestUnbufferedDispatch() untuk meminta agar sistem input tidak mengelompokkan kejadian gerakan tetapi mengirimkan segera setelah tersedia:

when (motionEvent.action) {
   MotionEvent.ACTION_DOWN -> {
       // Deliver input events as soon as they arrive.
       view.requestUnbufferedDispatch(motionEvent)
       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_MOVE -> {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_UP -> {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit()
   }
   MotionEvent.CANCEL -> {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel()
   }
}

Merendering anjuran dan larangan

✓ Anjuran

Sebagian kecil layar, tulisan tangan, gambar, dan sketsa.

✗ Larangan

Pembaruan layar penuh, penggeseran, zoom. Dapat mengakibatkan tearing.

Tearing

Tearing terjadi saat layar dimuat ulang saat buffer layar sedang diubah secara bersamaan. Sebagian layar menampilkan data baru, sementara layar yang lain menampilkan data lama.

Bagian atas dan bawah gambar Android tidak sejajar karena adanya tearing saat layar dimuat ulang.
Gambar 11. Tearing terjadi saat layar dimuat ulang dari atas ke bawah.

Prediksi gerakan

Prediksi gerakan Jetpack library mengurangi latensi yang dirasakan dengan memperkirakan jalur {i>stroke<i} pengguna dan memberikan titik buatan ke perender.

Library prediksi gerakan mendapatkan input pengguna yang sebenarnya sebagai objek MotionEvent. Objek berisi informasi tentang koordinat x dan y, tekanan, dan waktu, yang dimanfaatkan oleh prediktor gerakan untuk memprediksi MotionEvent mendatang objek terstruktur dalam jumlah besar.

Objek MotionEvent yang diprediksi hanyalah perkiraan. Peristiwa yang diprediksi dapat mengurangi latensi yang dirasakan, tetapi data yang diprediksi harus diganti dengan MotionEvent yang sebenarnya data setelah diterima.

Library prediksi gerakan tersedia mulai dari Android 4.4 (API level 19) dan yang lebih tinggi dan di perangkat ChromeOS yang menjalankan Android 9 (level API 28) dan yang lebih tinggi.

Latensi menyebabkan goresan yang dirender tertinggal di belakang posisi stilus. Jeda antara goresan dan stilus diisi dengan titik prediksi. Jeda yang tersisa adalah latensi yang dirasakan.
Gambar 12. Latensi dikurangi oleh prediksi gerakan.

Dependensi

Library prediksi gerakan menyediakan implementasi prediksi. Tujuan library ditambahkan sebagai dependensi dalam file build.gradle modul aplikasi:

dependencies {
    implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}

Implementasi

Library prediksi gerakan menyertakan MotionEventPredictor , yang menentukan metode berikut:

  • record(): Menyimpan objek MotionEvent sebagai catatan tindakan pengguna
  • predict(): Menampilkan prediksi MotionEvent
Deklarasikan instance MotionEventPredictor
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Melakukan feed pada prediktor menggunakan data
motionEventPredictor.record(motionEvent)
Prediksi

when (motionEvent.action) {
   MotionEvent.ACTION_MOVE -> {
       val predictedMotionEvent = motionEventPredictor?.predict()
       if(predictedMotionEvent != null) {
            // use predicted MotionEvent to inject a new artificial point
       }
   }
}

Anjuran dan larangan terkait gerakan

✓ Anjuran

Hapus titik prediksi jika titik prediksi baru ditambahkan.

✗ Larangan

Jangan gunakan titik prediksi untuk rendering akhir.

Aplikasi pencatatan

ChromeOS memungkinkan aplikasi Anda mendeklarasikan beberapa tindakan pencatatan.

Untuk mendaftarkan aplikasi sebagai aplikasi pencatatan di ChromeOS, lihat Input kompatibilitas yang berbeda.

Untuk mendaftarkan aplikasi sebagai aplikasi pencatatan di Android, lihat Membuat pencatatan aplikasi.

Android 14 (level API 34), memperkenalkan ACTION_CREATE_NOTE , yang memungkinkan aplikasi Anda memulai aktivitas pencatatan pada kunci layar.

Pengenalan tinta digital dengan ML Kit

Dengan tinta digital ML Kit pengenalan objek, aplikasi Anda dapat mengenali teks tulisan tangan pada platform digital dalam ratusan bahasa. Anda juga dapat mengklasifikasikan sketsa.

ML Kit menyediakan Ink.Stroke.Builder class untuk membuat objek Ink yang dapat diproses oleh model machine learning untuk mengonversi tulisan tangan menjadi teks.

Selain pengenalan tulis tangan, model ini dapat mengenali gestur, seperti hapus dan lingkari.

Lihat Tinta digital pengenalan untuk mempelajari lebih lanjut.

Referensi lainnya

Panduan developer

Codelab