1. Sebelum memulai
Stilus adalah alat berbentuk pena yang membantu pengguna melakukan tugas yang presisi. Dalam codelab ini, Anda akan mempelajari cara menerapkan pengalaman stilus organik dengan library android.os dan androidx. Anda juga akan mempelajari cara menggunakan class MotionEvent untuk mendukung tekanan, kemiringan, dan orientasi, serta penolakan telapak tangan untuk mencegah sentuhan yang tidak diinginkan. Selain itu, Anda akan mempelajari cara mengurangi latensi stilus dengan prediksi gerakan dan grafis latensi rendah dengan OpenGL dan class SurfaceView.
Prasyarat
- Pengalaman dengan Kotlin dan lambda.
- Pengetahuan dasar tentang cara menggunakan Android Studio.
- Pengetahuan dasar tentang Jetpack Compose.
- Pemahaman dasar tentang OpenGL untuk grafis latensi rendah.
Yang akan Anda pelajari
- Cara menggunakan class
MotionEventuntuk stilus. - Cara menerapkan kemampuan stilus, termasuk dukungan untuk tekanan, kemiringan, dan orientasi.
- Cara menggambar di class
Canvas. - Cara mengimplementasikan prediksi gerakan.
- Cara merender grafis latensi rendah dengan OpenGL dan class
SurfaceView.
Yang Anda butuhkan
- Versi terbaru Android Studio.
- Pengalaman dengan sintaksis Kotlin, termasuk lambda.
- Pengalaman dasar dengan Compose. Jika Anda tidak terbiasa dengan Compose, selesaikan codelab Dasar-dasar Jetpack Compose.
- Perangkat dengan dukungan stilus.
- Stilus aktif.
- Git.
2. Mendapatkan kode awal
Untuk mendapatkan kode yang berisi tema dan penyiapan dasar aplikasi awal, ikuti langkah-langkah berikut:
- Clone repositori GitHub ini:
git clone https://github.com/android/large-screen-codelabs
- Buka folder
advanced-stylus. Folderstartberisi kode awal dan folderendberisi kode solusi.
3. Mengimplementasikan aplikasi menggambar dasar
Pertama, Anda membuat tata letak yang diperlukan untuk aplikasi menggambar dasar yang memungkinkan pengguna menggambar, dan menampilkan atribut stilus di layar dengan fungsi Canvas Composable. Tampilannya akan terlihat seperti gambar berikut:

Bagian atas adalah fungsi Canvas Composable tempat Anda menggambar visualisasi stilus, dan menampilkan berbagai atribut stilus seperti orientasi, kemiringan, dan tekanan. Bagian bawah adalah fungsi Canvas Composable lain yang menerima input stilus dan menggambar goresan sederhana.
Untuk menerapkan tata letak dasar aplikasi gambar, ikuti langkah-langkah berikut:
- Di Android Studio, buka repositori yang di-clone.
- Klik
app>java>com.example.stylus, lalu klik dua kaliMainActivity. FileMainActivity.ktakan terbuka. - Di class
MainActivity, perhatikan fungsiStylusVisualizationdanDrawAreaComposable. Anda akan berfokus pada fungsiDrawAreaComposabledi bagian ini.
Membuat class StylusState.
- Di direktori
uiyang sama, klik File > New > Kotlin/Class file. - Di kotak teks, ganti placeholder Name dengan
StylusState.kt, lalu tekanEnter(ataureturndi macOS). - Di file
StylusState.kt, buat class dataStylusState, lalu tambahkan variabel dari tabel berikut:
Variabel | Jenis | Nilai default | Deskripsi |
|
| Nilai yang berkisar dari 0 hingga 1.0. | |
|
| Nilai radian yang berkisar dari -pi hingga pi. | |
|
| Nilai radian yang berkisar dari 0 hingga pi/2. | |
|
| Menyimpan baris yang dirender oleh fungsi |
StylusState.kt
package com.example.stylus.ui
import androidx.compose.ui.graphics.Path
data class StylusState(
var pressure: Float = 0F,
var orientation: Float = 0F,
var tilt: Float = 0F,
var path: Path = Path(),
)

- Di file
MainActivity.kt, temukan classMainActivity, lalu tambahkan status stilus dengan fungsimutableStateOf():
MainActivity.kt
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import com.example.stylus.ui.StylusState
class MainActivity : ComponentActivity() {
private var stylusState: StylusState by mutableStateOf(StylusState())
Class DrawPoint
Class DrawPoint menyimpan data tentang setiap titik yang digambar di layar; saat menghubungkan titik ini Anda membuat garis. Hal ini meniru cara kerja objek Path.
Class DrawPoint memperluas class PointF. Isinya adalah data berikut:
Parameter | Jenis | Deskripsi |
|
| Coordinate |
|
| Coordinate |
|
| Jenis titik |
Ada dua jenis objek DrawPoint, yang dijelaskan oleh enum DrawPointType:
Jenis | Deskripsi |
| Memindahkan awal garis ke suatu posisi. |
| Melacak garis dari titik sebelumnya. |
DrawPoint.kt
import android.graphics.PointF
class DrawPoint(x: Float, y: Float, val type: DrawPointType): PointF(x, y)
Merender titik data ke jalur
Untuk aplikasi ini, class StylusViewModel menyimpan data garis, menyiapkan data untuk rendering, dan melakukan beberapa operasi pada objek Path untuk penolakan telapak tangan.
- Untuk menyimpan data garis, di class
StylusViewModel, buat daftar objekDrawPointyang dapat diubah:
StylusViewModel.kt
import androidx.lifecycle.ViewModel
import com.example.stylus.data.DrawPoint
class StylusViewModel : ViewModel() {private var currentPath = mutableListOf<DrawPoint>()
Untuk merender titik data ke jalur, ikuti langkah berikut:
- Di class
StylusViewModelfileStylusViewModel.kt, tambahkan fungsicreatePath. - Buat variabel
pathjenisPathdengan konstruktorPath(). - Buat loop
fortempat Anda melakukan iterasi melalui setiap titik data dalam variabelcurrentPath. - Jika titik data adalah jenis
START, panggil metodemoveTountuk memulai garis pada koordinatxdanyyang ditentukan. - Jika tidak, panggil metode
lineTodengan koordinatxdanytitik data untuk menghubungkan ke titik sebelumnya. - Tampilkan objek
path.
StylusViewModel.kt
import androidx.compose.ui.graphics.Path
import com.example.stylus.data.DrawPoint
import com.example.stylus.data.DrawPointType
class StylusViewModel : ViewModel() {
private var currentPath = mutableListOf<DrawPoint>()
private fun createPath(): Path {
val path = Path()
for (point in currentPath) {
if (point.type == DrawPointType.START) {
path.moveTo(point.x, point.y)
} else {
path.lineTo(point.x, point.y)
}
}
return path
}
private fun cancelLastStroke() {
}
Memproses objek MotionEvent
Peristiwa stilus berasal dari objek MotionEvent, yang memberikan informasi tentang tindakan yang dilakukan dan data yang terkait dengannya, seperti posisi pointer dan tekanannya. Tabel berikut berisi beberapa konstanta objek MotionEvent dan datanya, yang dapat Anda gunakan untuk mengidentifikasi tindakan yang dilakukan pengguna di layar:
Konstanta | Data |
| Pointer menyentuh layar. Ini adalah awal garis pada posisi yang dilaporkan oleh objek |
| Pointer bergerak di layar. Ini adalah garis yang digambar. |
| Pointer berhenti menyentuh layar. Ini adalah akhir garis. |
| Sentuhan yang tidak diinginkan terdeteksi. Membatalkan goresan terakhir. |
Saat aplikasi menerima objek MotionEvent baru, layar harus dirender untuk mencerminkan input pengguna baru.
- Untuk memproses objek
MotionEventdi classStylusViewModel, buat fungsi yang mengumpulkan koordinat garis:
StylusViewModel.kt
import android.view.MotionEvent
class StylusViewModel : ViewModel() {
private var currentPath = mutableListOf<DrawPoint>()
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
currentPath.add(
DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.START)
)
}
MotionEvent.ACTION_MOVE -> {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
MotionEvent.ACTION_UP -> {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
MotionEvent.ACTION_CANCEL -> {
// Unwanted touch detected.
cancelLastStroke()
}
else -> return false
}
return true
}
Mengirim data ke UI
Untuk mengupdate class StylusViewModel agar UI dapat mengumpulkan perubahan di class data StylusState, ikuti langkah-langkah berikut:
- Di class
StylusViewModel, buat variabel_stylusStatedari jenisMutableStateFlowclassStylusState, dan variabelstylusStatedari jenisStateFlowStylusState. Variabel_stylusStatediubah setiap kali status stilus diubah di classStylusViewModeldan variabelstylusStatedigunakan oleh UI di classMainActivity.
StylusViewModel.kt
import com.example.stylus.ui.StylusState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
- Buat fungsi
requestRenderingyang menerima parameter objekStylusState:
StylusViewModel.kt
import kotlinx.coroutines.flow.update
...
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
...
private fun requestRendering(stylusState: StylusState) {
// Updates the stylusState, which triggers a flow.
_stylusState.update {
return@update stylusState
}
}
- Di akhir fungsi
processMotionEvent, tambahkan panggilan fungsirequestRenderingdengan parameterStylusState. - Di parameter
StylusState, ambil nilai kemiringan, tekanan, dan orientasi dari variabelmotionEvent, lalu buat jalur dengan fungsicreatePath(). Tindakan ini akan memicu peristiwa alur, yang akan Anda hubungkan di UI nanti.
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
else -> return false
}
requestRendering(
StylusState(
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
path = createPath()
)
)
Menautkan UI dengan class StylusViewModel
- Di class
MainActivity, temukan fungsisuper.onCreatedari fungsionCreate, lalu tambahkan pengumpulan status. Untuk mempelajari pengumpulan status lebih lanjut, lihat Mengumpulkan alur dengan cara yang mendukung siklus proses.
MainActivity.kt
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.onEach
import androidx.lifecycle.Lifecycle
import kotlinx.coroutines.flow.collect
...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stylusState
.onEach {
stylusState = it
}
.collect()
}
}
Sekarang, setiap kali class StylusViewModel memposting status StylusState baru, aktivitas akan menerimanya dan objek StylusState baru akan memperbarui variabel stylusState class MainActivity lokal.
- Dalam isi fungsi
DrawAreaComposable, tambahkan pengubahpointerInteropFilterke fungsiCanvasComposableuntuk menyediakan objekMotionEvent.
- Kirim objek
MotionEventke fungsiprocessMotionEventStylusViewModel untuk diproses:
MainActivity.kt
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.pointerInteropFilter
...
class MainActivity : ComponentActivity() {
...
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
Canvas(modifier = modifier
.clipToBounds()
.pointerInteropFilter {
viewModel.processMotionEvent(it)
}
) {
}
}
- Panggil fungsi
drawPathdengan atributstylusStatepath, lalu berikan gaya warna dan goresan.
MainActivity.kt
class MainActivity : ComponentActivity() {
...
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
Canvas(modifier = modifier
.clipToBounds()
.pointerInteropFilter {
viewModel.processMotionEvent(it)
}
) {
with(stylusState) {
drawPath(
path = this.path,
color = Color.Gray,
style = strokeStyle
)
}
}
}
- Jalankan aplikasi, lalu perhatikan bahwa Anda dapat menggambar di layar.
4. Mengimplementasikan dukungan untuk tekanan, orientasi, dan kemiringan
Di bagian sebelumnya, Anda telah melihat cara mengambil informasi stilus dari objek MotionEvent, seperti tekanan, orientasi, dan kemiringan.
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
Namun, pintasan ini hanya berfungsi untuk pointer pertama. Saat multi-sentuh terdeteksi, beberapa pointer akan terdeteksi dan pintasan ini hanya akan menampilkan nilai untuk pointer pertama—atau pointer pertama di layar. Untuk meminta data tentang pointer tertentu, Anda dapat menggunakan parameter pointerIndex:
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex),
pressure = motionEvent.getPressure(pointerIndex),
orientation = motionEvent.getOrientation(pointerIndex)
Untuk mempelajari pointer dan multisentuh lebih lanjut, lihat Menangani gestur multi-kontrol.
Menambahkan visualisasi untuk tekanan, orientasi, dan kemiringan
- Dalam file
MainActivity.kt, temukan fungsiStylusVisualizationComposable, lalu gunakan informasi untuk objek alurStylusStateuntuk merender visualisasi:
MainActivity.kt
import StylusVisualization.drawOrientation
import StylusVisualization.drawPressure
import StylusVisualization.drawTilt
...
class MainActivity : ComponentActivity() {
...
@Composable
fun StylusVisualization(modifier: Modifier = Modifier) {
Canvas(
modifier = modifier
) {
with(stylusState) {
drawOrientation(this.orientation)
drawTilt(this.tilt)
drawPressure(this.pressure)
}
}
}
- Jalankan aplikasi. Anda akan melihat tiga indikator di bagian atas layar yang menunjukkan orientasi, tekanan, dan kemiringan.
- Coret layar Anda dengan stilus, lalu amati reaksi setiap visualisasi terhadap input Anda.

- Periksa file
StylusVisualization.ktuntuk memahami cara pembuatan setiap visualisasi.
5. Mengimplementasikan penolakan telapak tangan
Layar dapat mendeteksi sentuhan yang tidak diinginkan. Misalnya, hal ini terjadi saat pengguna secara alami meletakkan tangannya di layar untuk penyangga saat menulis tangan.
Penolakan telapak tangan adalah mekanisme yang mendeteksi perilaku ini dan memberi tahu developer untuk membatalkan kumpulan objek MotionEvent terakhir. Kumpulan objek MotionEvent dimulai dengan konstanta ACTION_DOWN.
Artinya, Anda harus menyimpan histori input sehingga dapat menghapus sentuhan yang tidak diinginkan dari layar dan merender ulang input pengguna yang sah. Untungnya, Anda sudah memiliki histori yang disimpan di class StylusViewModel dalam variabel currentPath.
Android menyediakan konstanta ACTION_CANCEL dari objek MotionEvent untuk memberi tahu developer tentang sentuhan yang tidak diinginkan. Sejak Android 13, objek MotionEvent menyediakan konstanta FLAG_CANCELED yang harus diperiksa pada konstanta ACTION_POINTER_UP.
Mengimplementasikan fungsi cancelLastStroke
- Untuk menghapus titik data dari titik data
STARTterakhir, kembali ke classStylusViewModel, lalu buat fungsicancelLastStrokeyang menemukan indeks titik dataSTARTterakhir dan hanya menyimpan data dari titik data pertama hingga indeks minus satu:
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
private fun cancelLastStroke() {
// Find the last START event.
val lastIndex = currentPath.findLastIndex {
it.type == DrawPointType.START
}
// If found, keep the element from 0 until the very last event before the last MOVE event.
if (lastIndex > 0) {
currentPath = currentPath.subList(0, lastIndex - 1)
}
}
Menambahkan konstanta ACTION_CANCEL dan FLAG_CANCELED
- Di file
StylusViewModel.kt, temukan fungsiprocessMotionEvent. - Di konstanta
ACTION_UP, buat variabelcanceledyang memeriksa apakah versi SDK saat ini adalah Android 13 atau lebih tinggi, dan apakah konstantaFLAG_CANCELEDdiaktifkan. - Pada baris berikutnya, buat kondisional yang memeriksa apakah variabel
canceledsudah benar. Jika demikian, panggil fungsicancelLastStrokeuntuk menghapus kumpulan objekMotionEventterakhir. Jika tidak, panggil metodecurrentPath.adduntuk menambahkan kumpulan objekMotionEventterakhir.
StylusViewModel.kt
import android.os.Build
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_UP -> {
val canceled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
(motionEvent.flags and MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED
if(canceled) {
cancelLastStroke()
} else {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
}
- Dalam konstanta
ACTION_CANCEL, perhatikan fungsicancelLastStroke:
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_CANCEL -> {
// unwanted touch detected
cancelLastStroke()
}
Penolakan telapak tangan diterapkan. Anda dapat menemukan kode yang berfungsi di folder palm-rejection.
6. Mengimplementasikan latensi rendah
Di bagian ini, Anda akan mengurangi latensi antara input pengguna dan rendering layar untuk meningkatkan performa. Latensi memiliki beberapa penyebab dan salah satunya adalah pipeline grafis yang panjang. Anda mengurangi pipeline grafis dengan rendering buffer depan. Rendering buffer depan memberi developer akses langsung ke buffer layar, yang memberikan hasil luar biasa untuk tulisan tangan dan sketsa.
Class GLFrontBufferedRenderer yang disediakan oleh library androidx.graphics menangani rendering buffer depan dan ganda. Library ini mengoptimalkan objek SurfaceView untuk rendering cepat dengan fungsi callback onDrawFrontBufferedLayer dan rendering normal dengan fungsi callback onDrawDoubleBufferedLayer. Class GLFrontBufferedRenderer dan antarmuka GLFrontBufferedRenderer.Callback berfungsi dengan jenis data yang disediakan pengguna. Dalam codelab ini, Anda akan menggunakan class Segment.
Untuk memulai, ikuti langkah-langkah ini:
- Di Android Studio, buka folder
low-latencyagar Anda mendapatkan semua file yang diperlukan: - Perhatikan file baru berikut dalam project:
- Dalam file
build.gradle, libraryandroidx.graphicstelah diimpor dengan deklarasiimplementation "androidx.graphics:graphics-core:1.0.0-alpha03". - Class
LowLatencySurfaceViewmemperluas classSurfaceViewuntuk merender kode OpenGL di layar. - Class
LineRenderermenyimpan kode OpenGL untuk merender baris di layar. - Class
FastRenderermemungkinkan rendering cepat dan mengimplementasikan antarmukaGLFrontBufferedRenderer.Callback. Kode ini juga mencegat objekMotionEvent. - Class
StylusViewModelmenyimpan titik data dengan antarmukaLineManager. - Class
Segmentmenentukan segmen sebagai berikut: x1,y1: koordinat titik pertamax2,y2: koordinat titik kedua
Gambar berikut menunjukkan cara data berpindah di antara setiap class:

Membuat platform dan tata letak latensi rendah
- Di file
MainActivity.kt, temukan fungsionCreateclassMainActivity. - Dalam isi fungsi
onCreate, buat objekFastRenderer, lalu teruskan objekviewModel:
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fastRendering = FastRenderer(viewModel)
lifecycleScope.launch {
...
- Dalam file yang sama, buat fungsi
DrawAreaLowLatencyComposable. - Dalam isi fungsi, gunakan
AndroidViewAPI untuk menggabungkan tampilanLowLatencySurfaceView, lalu berikan objekfastRendering:
MainActivity.kt
import androidx.compose.ui.viewinterop.AndroidView
import com.example.stylus.gl.LowLatencySurfaceView
class MainActivity : ComponentActivity() {
...
@Composable
fun DrawAreaLowLatency(modifier: Modifier = Modifier) {
AndroidView(factory = { context ->
LowLatencySurfaceView(context, fastRenderer = fastRendering)
}, modifier = modifier)
}
- Di fungsi
onCreatesetelah fungsiDividerComposable, tambahkan fungsiDrawAreaLowLatencyComposableke tata letak:
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
Surface(
modifier = Modifier
.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
StylusVisualization(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
)
Divider(
thickness = 1.dp,
color = Color.Black,
)
DrawAreaLowLatency()
}
}
- Di direktori
gl, buka fileLowLatencySurfaceView.kt, lalu perhatikan baris berikut di classLowLatencySurfaceView:
- Class
LowLatencySurfaceViewmemperluas classSurfaceView. Class tersebut menggunakan metodeonTouchListenerobjekfastRenderer. - Antarmuka
GLFrontBufferedRenderer.Callbackmelalui classfastRendererharus dilampirkan ke objekSurfaceViewsaat fungsionAttachedToWindowdipanggil sehingga callback dapat dirender ke tampilanSurfaceView. - Antarmuka
GLFrontBufferedRenderer.Callbackmelalui classfastRendererharus dirilis saat fungsionDetachedFromWindowdipanggil.
LowLatencySurfaceView.kt
class LowLatencySurfaceView(context: Context, private val fastRenderer: FastRenderer) :
SurfaceView(context) {
init {
setOnTouchListener(fastRenderer.onTouchListener)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
fastRenderer.attachSurfaceView(this)
}
override fun onDetachedFromWindow() {
fastRenderer.release()
super.onDetachedFromWindow()
}
}
Menangani objek MotionEvent dengan antarmuka onTouchListener
Untuk menangani objek MotionEvent saat konstanta ACTION_DOWN terdeteksi, ikuti langkah-langkah berikut:
- Dalam direktori
gl, buka fileFastRenderer.kt. - Dalam isi konstanta
ACTION_DOWN, buat variabelcurrentXyang menyimpan koordinatxobjekMotionEventdan variabelcurrentYyang menyimpan koordinaty-nya. - Buat variabel
Segmentyang menyimpan objekSegmentyang menerima dua instance parametercurrentXdan dua instance parametercurrentYkarena itu merupakan awal baris. - Panggil metode
renderFrontBufferedLayerdengan parametersegmentuntuk memicu callback pada fungsionDrawFrontBufferedLayer.
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_DOWN -> {
// Ask that the input system not batch MotionEvent objects,
// but instead deliver them as soon as they're available.
view.requestUnbufferedDispatch(event)
currentX = event.x
currentY = event.y
// Create a single point.
val segment = Segment(currentX, currentY, currentX, currentY)
frontBufferRenderer?.renderFrontBufferedLayer(segment)
}
Untuk menangani objek MotionEvent saat konstanta ACTION_MOVE terdeteksi, ikuti langkah-langkah berikut:
- Dalam isi konstanta
ACTION_MOVE, buat variabelpreviousXyang menyimpan variabelcurrentXdan variabelpreviousYyang menyimpan variabelcurrentY. - Buat variabel
currentXyang menyimpan koordinatxobjekMotionEventsaat ini dan variabelcurrentYyang menyimpan koordinatysaat ini. - Buat variabel
Segmentyang menyimpan objekSegmentyang menerima parameterpreviousX,previousY,currentX, dancurrentY. - Panggil metode
renderFrontBufferedLayerdengan parametersegmentuntuk memicu callback pada fungsionDrawFrontBufferedLayerdan menjalankan kode OpenGL.
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_MOVE -> {
previousX = currentX
previousY = currentY
currentX = event.x
currentY = event.y
val segment = Segment(previousX, previousY, currentX, currentY)
// Send the short line to front buffered layer: fast rendering
frontBufferRenderer?.renderFrontBufferedLayer(segment)
}
- Untuk menangani objek
MotionEventsaat konstantaACTION_UPterdeteksi, panggil metodecommituntuk memicu panggilan pada fungsionDrawDoubleBufferedLayerdan menjalankan kode OpenGL:
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_UP -> {
frontBufferRenderer?.commit()
}
Mengimplementasikan fungsi callback GLFrontBufferedRenderer
Dalam file FastRenderer.kt, fungsi callback onDrawFrontBufferedLayer dan onDrawDoubleBufferedLayer mengeksekusi kode OpenGL. Di awal setiap fungsi callback, fungsi OpenGL berikut akan memetakan data Android ke ruang kerja OpenGL:
- Fungsi
GLES20.glViewportmenentukan ukuran persegi panjang tempat Anda merender tampilan. - Fungsi
Matrix.orthoMmenghitung matriksModelViewProjection. - Fungsi
Matrix.multiplyMMmelakukan perkalian matriks untuk mengubah data Android ke referensi OpenGL, dan memberikan penyiapan untuk matriksprojection.
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDraw[Front/Double]BufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
val bufferWidth = bufferInfo.width
val bufferHeight = bufferInfo.height
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
// Map Android coordinates to OpenGL coordinates.
Matrix.orthoM(
mvpMatrix,
0,
0f,
bufferWidth.toFloat(),
0f,
bufferHeight.toFloat(),
-1f,
1f
)
Matrix.multiplyMM(projection, 0, mvpMatrix, 0, transform, 0)
Setelah bagian kode tersebut disiapkan, Anda dapat berfokus pada kode yang melakukan rendering sebenarnya. Fungsi callback onDrawFrontBufferedLayer merender area kecil di layar. Fungsi ini memberikan nilai param dari jenis Segment sehingga Anda dapat merender satu segmen dengan cepat. Class LineRenderer adalah perender openGL untuk kuas yang menerapkan warna dan ukuran garis.
Untuk menerapkan fungsi callback onDrawFrontBufferedLayer, ikuti langkah-langkah berikut:
- Dalam file
FastRenderer.kt, temukan fungsi callbackonDrawFrontBufferedLayer. - Dalam isi fungsi callback
onDrawFrontBufferedLayer, panggil fungsiobtainRendereruntuk mendapatkan instanceLineRenderer. - Panggil metode
drawLinefungsiLineRendererdengan parameter berikut:
- Matriks
projectionsebelumnya telah dihitung. - Daftar objek
Segment, yang merupakan segmen tunggal dalam kasus ini. colorgaris.
FastRenderer.kt
import android.graphics.Color
import androidx.core.graphics.toColor
class FastRenderer( ... ) {
...
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
Matrix.multiplyMM(projection, 0, mvpMatrix, 0, transform, 0)
obtainRenderer().drawLine(projection, listOf(param), Color.GRAY.toColor())
}
- Jalankan aplikasi, lalu perhatikan bahwa Anda dapat menggambar pada layar dengan latensi minimum. Namun, aplikasi tidak akan mempertahankan garis tersebut karena Anda masih perlu mengimplementasikan fungsi callback
onDrawDoubleBufferedLayer.
Fungsi callback onDrawDoubleBufferedLayer dipanggil setelah fungsi commit untuk memungkinkan persistensi baris. Callback memberikan nilai params, yang berisi kumpulan objek Segment. Semua segmen di buffer depan diulang di buffer ganda untuk persistensi.
Untuk menerapkan fungsi callback onDrawDoubleBufferedLayer, ikuti langkah-langkah berikut:
- Dalam file
StylusViewModel.kt, temukan classStylusViewModel, lalu buat variabelopenGlLinesyang menyimpan daftar objekSegmentyang dapat diubah:
StylusViewModel.kt
import com.example.stylus.data.Segment
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
val openGlLines = mutableListOf<List<Segment>>()
private fun requestRendering(stylusState: StylusState) {
- Dalam file
FastRenderer.kt, temukan fungsi callbackonDrawDoubleBufferedLayerclassFastRenderer. - Dalam isi fungsi callback
onDrawDoubleBufferedLayer, hapus layar dengan metodeGLES20.glClearColordanGLES20.glClearagar scene dapat dirender dari awal, serta tambahkan baris ke objekviewModeluntuk mempertahankannya:
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
// Clear the screen with black.
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
viewModel.openGlLines.add(params.toList())
- Buat loop
foryang melakukan iterasi dan merender setiap baris dari objekviewModel:
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
// Clear the screen with black.
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
viewModel.openGlLines.add(params.toList())
// Render the entire scene (all lines).
for (line in viewModel.openGlLines) {
obtainRenderer().drawLine(projection, line, Color.GRAY.toColor())
}
}
- Jalankan aplikasi, lalu perhatikan bahwa Anda dapat menggambar di layar, dan garis dipertahankan setelah konstanta
ACTION_UPdipicu.
7. Mengimplementasikan prediksi gerakan
Anda dapat meningkatkan latensi lebih lanjut dengan library androidx.input, yang menganalisis arah stilus, dan memprediksi lokasi titik berikutnya dan menyisipkannya untuk rendering.
Untuk menyiapkan prediksi gerakan, ikuti langkah-langkah ini:
- Dalam file
app/build.gradle, impor library di bagian dependensi:
app/build.gradle
...
dependencies {
...
implementation"androidx.input:input-motionprediction:1.0.0-beta01"
- Klik File > Sync project with Gradle files.
- Di class
FastRenderingfileFastRendering.kt, deklarasikan objekmotionEventPredictorsebagai atribut:
FastRenderer.kt
import androidx.input.motionprediction.MotionEventPredictor
class FastRenderer( ... ) {
...
private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
private var motionEventPredictor: MotionEventPredictor? = null
- Dalam fungsi
attachSurfaceView, lakukan inisialisasi variabelmotionEventPredictor:
FastRenderer.kt
class FastRenderer( ... ) {
...
fun attachSurfaceView(surfaceView: SurfaceView) {
frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
}
- Di variabel
onTouchListener, panggil metodemotionEventPredictor?.recordsehingga objekmotionEventPredictormendapatkan data gerakan:
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
Langkah berikutnya adalah memprediksi objek MotionEvent dengan fungsi predict. Sebaiknya prediksi kapan konstanta ACTION_MOVE diterima dan setelah objek MotionEvent dicatat. Dengan kata lain, Anda harus memprediksi kapan stroke sedang berlangsung.
- Prediksi objek
MotionEventbuatan dengan metodepredict. - Buat objek
Segmentyang menggunakan koordinat x dan y saat ini dan yang diprediksi. - Minta rendering cepat dari segmen yang diprediksi dengan metode
frontBufferRenderer?.renderFrontBufferedLayer(predictedSegment).
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
...
frontBufferRenderer?.renderFrontBufferedLayer(segment)
val motionEventPredicted = motionEventPredictor?.predict()
if(motionEventPredicted != null) {
val predictedSegment = Segment(currentX, currentY,
motionEventPredicted.x, motionEventPredicted.y)
frontBufferRenderer?.renderFrontBufferedLayer(predictedSegment)
}
}
...
}
Peristiwa yang diprediksi disisipkan untuk dirender, yang meningkatkan latensi.
- Jalankan aplikasi, lalu perhatikan latensi yang ditingkatkan.
Meningkatkan latensi akan memberikan pengalaman stilus yang lebih alami kepada pengguna stilus.
8. Selamat
Selamat! Sekarang Anda tahu cara menangani stilus seperti seorang profesional.
Anda telah mempelajari cara memproses objek MotionEvent untuk mengekstrak informasi tentang tekanan, orientasi, dan kemiringan. Anda juga telah mempelajari cara meningkatkan latensi dengan mengimplementasikan library androidx.graphics dan library androidx.input. Peningkatan ini diterapkan bersama-sama untuk menawarkan pengalaman stilus yang lebih organik.