Android dan ChromeOS menyediakan berbagai API untuk membantu Anda mem-build aplikasi yang menawarkan
pengalaman stilus yang luar biasa kepada pengguna. Class
MotionEvent
menampilkan
informasi tentang interaksi stilus dengan layar, seperti tekanan stilus,
orientasi, kemiringan, pengarahan kursor, dan deteksi telapak tangan. Library grafis dan prediksi gerakan
berlatensi rendah meningkatkan rendering stilus di layar untuk memberikan
pengalaman yang alami seperti pena dan kertas.
MotionEvent
Class MotionEvent
mewakili interaksi input pengguna seperti posisi
dan gerakan 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 peristiwa UI
berikut:
- Tindakan: Interaksi fisik dengan perangkat—menyentuh layar, menggerakkan kursor ke permukaan layar,
- Pointer: ID objek yang berinteraksi dengan layar—jari, stilus, mouse
- Sumbu: Jenis data—koordinat x dan y, tekanan, kemiringan, orientasi, dan pengarahan kursor (jarak)
Tindakan
Untuk menerapkan dukungan stilus, Anda harus memahami tindakan yang dilakukan pengguna.
MotionEvent
menyediakan berbagai konstanta ACTION
yang menentukan peristiwa
gerakan. 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 saat
ACTION_UP
dipicu.
Kumpulan tindakan MotionEvent
dari ACTION_DOWN
hingga ACTION_UP
untuk pointer
tertentu disebut kumpulan gerakan.
Pointer
Sebagian besar layar multi-sentuh: sistem menetapkan pointer untuk setiap jari, stilus, mouse, atau objek pointer lainnya yang berinteraksi dengan layar. Indeks pointer 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()
minus 1.
Nilai sumbu pointer dapat diakses dengan metode getAxisValue(axis,
pointerIndex)
.
Jika indeks pointer dihilangkan, sistem akan menampilkan nilai untuk pointer pertama, pointer nol (0).
Objek MotionEvent
berisi informasi tentang jenis pointer yang digunakan. Anda
bisa mendapatkan jenis pointer dengan melakukan iterasi melalui indeks pointer dan memanggil
metode
getToolType(pointerIndex)
.
Untuk mempelajari pointer lebih lanjut, lihat Menangani gestur multisentuh.
Input stilus
Anda dapat memfilter input stilus dengan
TOOL_TYPE_STYLUS
:
Kotlin
val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)
Java
boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);
Stilus juga dapat melaporkan bahwa stilus digunakan sebagai penghapus dengan
TOOL_TYPE_ERASER
:
Kotlin
val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)
Java
boolean isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex);
Data sumbu stilus
ACTION_DOWN
dan ACTION_MOVE
memberikan data sumbu tentang stilus, yaitu koordinat x dan
y, tekanan, orientasi, kemiringan, dan pengarahan kursor.
Untuk mengaktifkan akses ke data ini, MotionEvent
API menyediakan
getAxisValue(int)
,
dengan parameternya merupakan 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 gestur multi-kontrol.
Posisi
Anda dapat mengambil koordinat x dan y pointer dengan panggilan berikut:
MotionEvent#getAxisValue(AXIS_X)
atauMotionEvent#getX()
MotionEvent#getAxisValue(AXIS_Y)
atauMotionEvent#getY()
Tekanan
Anda dapat mengambil tekanan pointer dengan
MotionEvent#getAxisValue(AXIS_PRESSURE)
atau, untuk pointer pertama,
MotionEvent#getPressure()
.
Nilai tekanan untuk layar sentuh atau touchpad adalah nilai antara 0 (tanpa tekanan) dan 1, tetapi nilai yang lebih tinggi dapat ditampilkan, bergantung pada kalibrasi layar.
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 hingga -pi berlawanan arah jarum jam.
Orientasi memungkinkan Anda menerapkan kuas sungguhan. Misalnya, jika stilus mewakili kuas datar, lebar kuas datar bergantung pada orientasi stilus.
Kemiringan
Kemiringan mengukur kemiringan stilus relatif terhadap layar.
Kemiringan menampilkan sudut positif stilus dalam radian, dengan nol 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.
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. Jarak
layang antara layar dan ujung pena (titik) stilus bergantung pada
produsen layar dan stilus. Karena implementasinya 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 tombol akan dipilih.
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, saat
pengguna secara alami meletakkan tangannya di layar untuk mendapatkan dukungan saat menulis tangan.
Penolakan telapak tangan adalah mekanisme yang mendeteksi perilaku ini dan memberi tahu Anda bahwa
MotionEvent
yang 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
dirancang untuk menginformasikan bahwa MotionEvent
yang ditetapkan sebelumnya harus
dibatalkan dari ACTION_DOWN
terakhir sehingga Anda, misalnya, dapat mengurungkan goresan terakhir
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. Flag ini
biasanya disetel saat pengguna tidak sengaja menyentuh layar, misalnya dengan menggenggam
perangkat atau meletakkan telapak tangan di layar.
Anda mengakses nilai flag sebagai berikut:
Kotlin
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
Java
boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;
Jika tanda ini disetel, Anda harus mengurungkan kumpulan MotionEvent
terakhir, dari
ACTION_DOWN
terakhir dari pointer ini.
Seperti ACTION_CANCEL
, pointer dapat ditemukan dengan getPointerId(actionIndex)
.
Gestur layar penuh, tata letak layar penuh, dan navigasi
Jika aplikasi dalam mode layar penuh dan memiliki elemen yang dapat ditindaklanjuti di dekat tepi, seperti kanvas aplikasi menggambar atau mencatat, menggeser dari bagian bawah layar untuk menampilkan navigasi atau memindahkan aplikasi ke latar belakang dapat mengakibatkan sentuhan yang tidak diinginkan pada kanvas.
Untuk mencegah gestur memicu sentuhan yang tidak diinginkan di aplikasi, Anda dapat
memanfaatkan inset dan
ACTION_CANCEL
.
Lihat juga bagian Penolakan telapak tangan, navigasi, dan input yang tidak diinginkan.
Gunakan metode
setSystemBarsBehavior()
dan
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
dari
WindowInsetsController
untuk mencegah gestur navigasi menyebabkan peristiwa sentuh yang tidak diinginkan:
Kotlin
// Configure the behavior of the hidden system bars. windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
Java
// Configure the behavior of the hidden system bars. windowInsetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE );
Untuk mempelajari pengelolaan inset dan gestur lebih lanjut, lihat:
- Menyembunyikan kolom sistem untuk mode imersif
- Memastikan kompatibilitas dengan navigasi gestur
- Menampilkan konten layar penuh di aplikasi Anda
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
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 per detik layar sentuh memeriksa apakah pointer menyentuh permukaan, berkisar antara 60 hingga 1000 Hz.
- Pemrosesan input (aplikasi): Menerapkan warna, efek grafis, dan transformasi pada 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 sehingga dapat menulis langsung ke layar.
Rendering buffer depan
Buffer depan adalah memori yang digunakan layar untuk rendering. Rendering buffer depan ini merupakan aplikasi terdekat yang bisa menggambar secara langsung ke layar. Library latensi rendah memungkinkan aplikasi 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).
Meskipun rendering buffer depan merupakan teknik yang baik untuk merender area kecil layar, rendering ini tidak dirancang untuk memuat ulang seluruh layar. Dengan rendering buffer depan, aplikasi merender konten ke buffer tempat layar dibaca. Karenanya, ada kemungkinan rendering artefak atau tearing (lihat di bawah).
Library latensi rendah tersedia dari Android 10 (API level 29) dan yang lebih tinggi, serta di perangkat ChromeOS yang menjalankan Android 10 (API level 29) dan yang lebih tinggi.
Dependensi
Library latensi rendah menyediakan komponen untuk implementasi rendering
buffer depan. Library ini ditambahkan sebagai dependensi dalam file build.gradle
modul
aplikasi:
dependencies {
implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}
Callback GLFrontBufferRenderer
Library latensi rendah menyertakan
antarmuka GLFrontBufferRenderer.Callback
, yang menentukan metode berikut:
Library latensi rendah tidak memiliki opini terkait jenis data yang Anda gunakan dengan
GLFrontBufferRenderer
.
Namun, library ini akan memproses data sebagai aliran dari ratusan titik data; oleh karena itu, desain data Anda agar dapat mengoptimalkan penggunaan dan alokasi memori.
Callback
Untuk mengaktifkan callback rendering, terapkan GLFrontBufferedRenderer.Callback
lalu
ganti onDrawFrontBufferedLayer()
dan onDrawDoubleBufferedLayer()
.
GLFrontBufferedRenderer
menggunakan callback untuk merender data Anda dengan cara yang paling
optimal.
Kotlin
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. } }
Java
GLFrontBufferedRenderer.Callback<DATA_TYPE> callbacks = new GLFrontBufferedRenderer.Callback<DATA_TYPE>() { @Override public void onDrawFrontBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, DATA_TYPE data_type) { // OpenGL for front buffer, short, affecting small area of the screen. } @Override public void onDrawDoubleBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, @NonNull Collection<? extends DATA_TYPE> collection) { // OpenGL full scene rendering. } };
Mendeklarasikan instance GLFrontBufferedRenderer
Siapkan GLFrontBufferedRenderer
dengan menyediakan SurfaceView
dan callback yang Anda buat sebelumnya. GLFrontBufferedRenderer
mengoptimalkan rendering
ke buffer depan dan buffer ganda menggunakan callback:
Kotlin
var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Java
GLFrontBufferedRenderer<DATA_TYPE> glFrontBufferRenderer = new GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks);
Rendering
Rendering buffer depan dimulai saat Anda memanggil
metode renderFrontBufferedLayer()
, yang memicu callback onDrawFrontBufferedLayer()
.
Rendering buffer ganda dilanjutkan saat Anda memanggil
fungsi commit()
, yang memicu callback onDrawMultiDoubleBufferedLayer()
.
Pada contoh berikut, proses dirender ke buffer depan (rendering
cepat) saat pengguna mulai menggambar di layar (ACTION_DOWN
) dan menggerakkan
pointer (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 peristiwa gerakan, tetapi segera mengirimkannya
setelah tersedia:
Kotlin
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() } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: { // Deliver input events as soon as they arrive. surfaceView.requestUnbufferedDispatch(motionEvent); // Pointer is in contact with the screen. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_MOVE: { // Pointer is moving. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_UP: { // Pointer is not in contact in the screen. glFrontBufferRenderer.commit(); } break; case MotionEvent.ACTION_CANCEL: { // Cancel front buffer; remove last motion set from the screen. glFrontBufferRenderer.cancel(); } break; }
Merendering anjuran dan larangan
Sebagian kecil layar, tulisan tangan, gambar, dan sketsa.
Pembaruan layar penuh, penggeseran, zoom. Dapat mengakibatkan tearing.
Tearing
Tearing terjadi saat layar dimuat ulang saat buffer layar diubah secara bersamaan. Sebagian layar menampilkan data baru, sementara layar yang lain menampilkan data lama.
Prediksi gerakan
Library prediksi gerakan Jetpack mengurangi latensi yang dirasakan dengan memperkirakan jalur goresan yang dibuat pengguna dan memberikan titik buatan sementara ke perender.
Library prediksi gerakan mendapatkan input pengguna yang sebenarnya sebagai objek MotionEvent
.
Objek ini berisi informasi tentang koordinat x dan y, tekanan, serta waktu, yang dimanfaatkan oleh prediktor gerakan untuk memprediksi objek MotionEvent
di masa mendatang.
Objek MotionEvent
yang diprediksi hanyalah perkiraan. Peristiwa yang diprediksi dapat mengurangi latensi yang dirasakan, tetapi data yang diprediksi harus diganti dengan data MotionEvent
sebenarnya setelah diterima.
Library prediksi gerakan tersedia mulai dari Android 4.4 (API level 19) dan yang lebih tinggi, serta di perangkat ChromeOS yang menjalankan Android 9 (API level 28) dan yang lebih tinggi.
Dependensi
Library prediksi gerakan menyediakan implementasi prediksi. Library
ditambahkan sebagai dependensi dalam file build.gradle
modul aplikasi:
dependencies {
implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}
Penerapan
Library prediksi gerakan menyertakan antarmuka
MotionEventPredictor
, yang menentukan metode berikut:
record()
: Menyimpan objekMotionEvent
sebagai catatan tindakan penggunapredict()
: Menampilkan prediksiMotionEvent
Deklarasikan instance MotionEventPredictor
Kotlin
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Java
MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
Melakukan feed pada prediktor menggunakan data
Kotlin
motionEventPredictor.record(motionEvent)
Java
motionEventPredictor.record(motionEvent);
Prediksi
Kotlin
when (motionEvent.action) { MotionEvent.ACTION_MOVE -> { val predictedMotionEvent = motionEventPredictor?.predict() if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: { MotionEvent predictedMotionEvent = motionEventPredictor.predict(); if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } break; }
Anjuran dan larangan terkait gerakan
Hapus titik prediksi jika titik prediksi baru ditambahkan.
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 Kompatibilitas input.
Untuk mendaftarkan aplikasi sebagai aplikasi pencatatan di Android, lihat Membuat aplikasi pencatatan.
Android 14 (level API 34), memperkenalkan intent
ACTION_CREATE_NOTE
, yang memungkinkan aplikasi Anda memulai aktivitas pencatatan di layar
kunci.
Pengenalan tinta digital dengan ML Kit
Dengan pengenalan tinta digital ML Kit, aplikasi Anda dapat mengenali teks tulisan tangan pada platform digital dalam ratusan bahasa. Anda juga dapat mengklasifikasikan sketsa.
ML Kit menyediakan class Ink.Stroke.Builder
untuk membuat objek Ink
yang dapat diproses oleh model machine learning guna mengonversi tulisan tangan menjadi teks.
Selain pengenalan tulis tangan, model ini dapat mengenali gestur, seperti hapus dan lingkaran.
Lihat Pengenalan tinta digital untuk mempelajari lebih lanjut.