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 pertama
- x2,- 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.
