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
MotionEvent
untuk 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
. Folderstart
berisi kode awal dan folderend
berisi 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.kt
akan terbuka. - Di class
MainActivity
, perhatikan fungsiStylusVisualization
danDrawArea
Composable
. Anda akan berfokus pada fungsiDrawArea
Composable
di bagian ini.
Membuat class StylusState
.
- Di direktori
ui
yang sama, klik File > New > Kotlin/Class file. - Di kotak teks, ganti placeholder Name dengan
StylusState.kt
, lalu tekanEnter
(ataureturn
di 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 objekDrawPoint
yang 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
StylusViewModel
fileStylusViewModel.kt
, tambahkan fungsicreatePath
. - Buat variabel
path
jenisPath
dengan konstruktorPath()
. - Buat loop
for
tempat Anda melakukan iterasi melalui setiap titik data dalam variabelcurrentPath
. - Jika titik data adalah jenis
START
, panggil metodemoveTo
untuk memulai garis pada koordinatx
dany
yang ditentukan. - Jika tidak, panggil metode
lineTo
dengan koordinatx
dany
titik 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
MotionEvent
di 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_stylusState
dari jenisMutableStateFlow
classStylusState
, dan variabelstylusState
dari jenisStateFlow
StylusState
. Variabel_stylusState
diubah setiap kali status stilus diubah di classStylusViewModel
dan variabelstylusState
digunakan 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
requestRendering
yang 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 fungsirequestRendering
dengan 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.onCreate
dari 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
DrawArea
Composable
, tambahkan pengubahpointerInteropFilter
ke fungsiCanvas
Composable
untuk menyediakan objekMotionEvent
.
- Kirim objek
MotionEvent
ke fungsiprocessMotionEvent
StylusViewModel 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
drawPath
dengan atributstylusState
path
, 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 fungsiStylusVisualization
Composable
, lalu gunakan informasi untuk objek alurStylusState
untuk 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.kt
untuk 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
START
terakhir, kembali ke classStylusViewModel
, lalu buat fungsicancelLastStroke
yang menemukan indeks titik dataSTART
terakhir 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 variabelcanceled
yang memeriksa apakah versi SDK saat ini adalah Android 13 atau lebih tinggi, dan apakah konstantaFLAG_CANCELED
diaktifkan. - Pada baris berikutnya, buat kondisional yang memeriksa apakah variabel
canceled
sudah benar. Jika demikian, panggil fungsicancelLastStroke
untuk menghapus kumpulan objekMotionEvent
terakhir. Jika tidak, panggil metodecurrentPath.add
untuk menambahkan kumpulan objekMotionEvent
terakhir.
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-latency
agar Anda mendapatkan semua file yang diperlukan: - Perhatikan file baru berikut dalam project:
- Dalam file
build.gradle
, libraryandroidx.graphics
telah diimpor dengan deklarasiimplementation "androidx.graphics:graphics-core:1.0.0-alpha03"
. - Class
LowLatencySurfaceView
memperluas classSurfaceView
untuk merender kode OpenGL di layar. - Class
LineRenderer
menyimpan kode OpenGL untuk merender baris di layar. - Class
FastRenderer
memungkinkan rendering cepat dan mengimplementasikan antarmukaGLFrontBufferedRenderer.Callback
. Kode ini juga mencegat objekMotionEvent
. - Class
StylusViewModel
menyimpan titik data dengan antarmukaLineManager
. - Class
Segment
menentukan 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 fungsionCreate
classMainActivity
. - 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
DrawAreaLowLatency
Composable
. - Dalam isi fungsi, gunakan
AndroidView
API 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
onCreate
setelah fungsiDivider
Composable
, tambahkan fungsiDrawAreaLowLatency
Composable
ke 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
LowLatencySurfaceView
memperluas classSurfaceView
. Class tersebut menggunakan metodeonTouchListener
objekfastRenderer
. - Antarmuka
GLFrontBufferedRenderer.Callback
melalui classfastRenderer
harus dilampirkan ke objekSurfaceView
saat fungsionAttachedToWindow
dipanggil sehingga callback dapat dirender ke tampilanSurfaceView
. - Antarmuka
GLFrontBufferedRenderer.Callback
melalui classfastRenderer
harus dirilis saat fungsionDetachedFromWindow
dipanggil.
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 variabelcurrentX
yang menyimpan koordinatx
objekMotionEvent
dan variabelcurrentY
yang menyimpan koordinaty
-nya. - Buat variabel
Segment
yang menyimpan objekSegment
yang menerima dua instance parametercurrentX
dan dua instance parametercurrentY
karena itu merupakan awal baris. - Panggil metode
renderFrontBufferedLayer
dengan parametersegment
untuk 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 variabelpreviousX
yang menyimpan variabelcurrentX
dan variabelpreviousY
yang menyimpan variabelcurrentY
. - Buat variabel
currentX
yang menyimpan koordinatx
objekMotionEvent
saat ini dan variabelcurrentY
yang menyimpan koordinaty
saat ini. - Buat variabel
Segment
yang menyimpan objekSegment
yang menerima parameterpreviousX
,previousY
,currentX
, dancurrentY
. - Panggil metode
renderFrontBufferedLayer
dengan parametersegment
untuk memicu callback pada fungsionDrawFrontBufferedLayer
dan 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
MotionEvent
saat konstantaACTION_UP
terdeteksi, panggil metodecommit
untuk memicu panggilan pada fungsionDrawDoubleBufferedLayer
dan 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.glViewport
menentukan ukuran persegi panjang tempat Anda merender tampilan. - Fungsi
Matrix.orthoM
menghitung matriksModelViewProjection
. - Fungsi
Matrix.multiplyMM
melakukan 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 fungsiobtainRenderer
untuk mendapatkan instanceLineRenderer
. - Panggil metode
drawLine
fungsiLineRenderer
dengan parameter berikut:
- Matriks
projection
sebelumnya telah dihitung. - Daftar objek
Segment
, yang merupakan segmen tunggal dalam kasus ini. color
garis.
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 variabelopenGlLines
yang menyimpan daftar objekSegment
yang 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 callbackonDrawDoubleBufferedLayer
classFastRenderer
. - Dalam isi fungsi callback
onDrawDoubleBufferedLayer
, hapus layar dengan metodeGLES20.glClearColor
danGLES20.glClear
agar scene dapat dirender dari awal, serta tambahkan baris ke objekviewModel
untuk 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
for
yang 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_UP
dipicu.
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
FastRendering
fileFastRendering.kt
, deklarasikan objekmotionEventPredictor
sebagai 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?.record
sehingga objekmotionEventPredictor
mendapatkan 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
MotionEvent
buatan dengan metodepredict
. - Buat objek
Segment
yang 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.