Meningkatkan dukungan stilus di aplikasi Android

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

2. Mendapatkan kode awal

Untuk mendapatkan kode yang berisi tema dan penyiapan dasar aplikasi awal, ikuti langkah-langkah berikut:

  1. Clone repositori GitHub ini:
git clone https://github.com/android/large-screen-codelabs
  1. Buka folder advanced-stylus. Folder start berisi kode awal dan folder end 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:

Aplikasi menggambar dasar. Bagian atas untuk visualisasi dan bagian bawah untuk menggambar.

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:

  1. Di Android Studio, buka repositori yang di-clone.
  2. Klik app > java > com.example.stylus, lalu klik dua kali MainActivity. File MainActivity.kt akan terbuka.
  3. Di class MainActivity, perhatikan fungsi StylusVisualization dan DrawArea Composable. Anda akan berfokus pada fungsi DrawArea Composable di bagian ini.

Membuat class StylusState.

  1. Di direktori ui yang sama, klik File > New > Kotlin/Class file.
  2. Di kotak teks, ganti placeholder Name dengan StylusState.kt, lalu tekan Enter (atau return di macOS).
  3. Di file StylusState.kt, buat class data StylusState, lalu tambahkan variabel dari tabel berikut:

Variabel

Jenis

Nilai default

Deskripsi

pressure

Float

0F

Nilai yang berkisar dari 0 hingga 1.0.

orientation

Float

0F

Nilai radian yang berkisar dari -pi hingga pi.

tilt

Float

0F

Nilai radian yang berkisar dari 0 hingga pi/2.

path

Path

Path()

Menyimpan baris yang dirender oleh fungsi Canvas Composable dengan metode drawPath.

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(),
)

Tampilan dasbor dari metrik orientasi, kemiringan, dan tekanan

  1. Di file MainActivity.kt, temukan class MainActivity, lalu tambahkan status stilus dengan fungsi mutableStateOf():

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

x

Float

Coordinate

y

Float

Coordinate

type

DrawPointType

Jenis titik

Ada dua jenis objek DrawPoint, yang dijelaskan oleh enum DrawPointType:

Jenis

Deskripsi

START

Memindahkan awal garis ke suatu posisi.

LINE

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 objek DrawPoint 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:

  1. Di class StylusViewModel file StylusViewModel.kt, tambahkan fungsi createPath.
  2. Buat variabel path jenis Path dengan konstruktor Path().
  3. Buat loop for tempat Anda melakukan iterasi melalui setiap titik data dalam variabel currentPath.
  4. Jika titik data adalah jenis START, panggil metode moveTo untuk memulai garis pada koordinat x dan y yang ditentukan.
  5. Jika tidak, panggil metode lineTo dengan koordinat x dan y titik data untuk menghubungkan ke titik sebelumnya.
  6. 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

ACTION_DOWN

Pointer menyentuh layar. Ini adalah awal garis pada posisi yang dilaporkan oleh objek MotionEvent.

ACTION_MOVE

Pointer bergerak di layar. Ini adalah garis yang digambar.

ACTION_UP

Pointer berhenti menyentuh layar. Ini adalah akhir garis.

ACTION_CANCEL

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 class StylusViewModel, 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:

  1. Di class StylusViewModel, buat variabel _stylusState dari jenis MutableStateFlow class StylusState, dan variabel stylusState dari jenis StateFlow StylusState. Variabel _stylusState diubah setiap kali status stilus diubah di class StylusViewModel dan variabel stylusState digunakan oleh UI di class MainActivity.

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
  1. Buat fungsi requestRendering yang menerima parameter objek StylusState:

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
      }
   }
  1. Di akhir fungsi processMotionEvent, tambahkan panggilan fungsi requestRendering dengan parameter StylusState.
  2. Di parameter StylusState, ambil nilai kemiringan, tekanan, dan orientasi dari variabel motionEvent, lalu buat jalur dengan fungsi createPath(). 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()
         )
      )
  1. Di class MainActivity, temukan fungsi super.onCreate dari fungsi onCreate, 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.

  1. Dalam isi fungsi DrawArea Composable, tambahkan pengubah pointerInteropFilter ke fungsi Canvas Composable untuk menyediakan objek MotionEvent.
  1. Kirim objek MotionEvent ke fungsi processMotionEvent 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)
          }

   ) {

   }
}
  1. Panggil fungsi drawPath dengan atribut stylusState 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
              )
          }
      }
   }
  1. 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

  1. Dalam file MainActivity.kt, temukan fungsi StylusVisualization Composable, lalu gunakan informasi untuk objek alur StylusState 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)
          }
      }
   }
  1. Jalankan aplikasi. Anda akan melihat tiga indikator di bagian atas layar yang menunjukkan orientasi, tekanan, dan kemiringan.
  2. Coret layar Anda dengan stilus, lalu amati reaksi setiap visualisasi terhadap input Anda.

Orientasi, tekanan, dan kemiringan yang divisualisasikan untuk kata 'halo' yang ditulis dengan stilus

  1. 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 class StylusViewModel, lalu buat fungsi cancelLastStroke yang menemukan indeks titik data START 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

  1. Di file StylusViewModel.kt, temukan fungsi processMotionEvent.
  2. Di konstanta ACTION_UP, buat variabel canceled yang memeriksa apakah versi SDK saat ini adalah Android 13 atau lebih tinggi, dan apakah konstanta FLAG_CANCELED diaktifkan.
  3. Pada baris berikutnya, buat kondisional yang memeriksa apakah variabel canceled sudah benar. Jika demikian, panggil fungsi cancelLastStroke untuk menghapus kumpulan objek MotionEvent terakhir. Jika tidak, panggil metode currentPath.add untuk menambahkan kumpulan objek MotionEvent 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))
           }
        }
  1. Dalam konstanta ACTION_CANCEL, perhatikan fungsi cancelLastStroke:

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:

  1. Di Android Studio, buka folder low-latency agar Anda mendapatkan semua file yang diperlukan:
  2. Perhatikan file baru berikut dalam project:
  • Dalam file build.gradle, library androidx.graphics telah diimpor dengan deklarasi implementation "androidx.graphics:graphics-core:1.0.0-alpha03".
  • Class LowLatencySurfaceView memperluas class SurfaceView untuk merender kode OpenGL di layar.
  • Class LineRenderer menyimpan kode OpenGL untuk merender baris di layar.
  • Class FastRenderer memungkinkan rendering cepat dan mengimplementasikan antarmuka GLFrontBufferedRenderer.Callback. Kode ini juga mencegat objek MotionEvent.
  • Class StylusViewModel menyimpan titik data dengan antarmuka LineManager.
  • Class Segment menentukan segmen sebagai berikut:
  • x1, y1: koordinat titik pertama
  • x2, y2: koordinat titik kedua

Gambar berikut menunjukkan cara data berpindah di antara setiap class:

MotionEvent ditangkap oleh LowLatensiSurfaceView dan dikirim ke onTouchListener untuk diproses. onTouchListener memproses dan meminta rendering buffer Depan atau Ganda ke GLFrontBufferRenderer. GLFrontBufferRenderer dirender ke LowLatensiSurfaceView.

Membuat platform dan tata letak latensi rendah

  1. Di file MainActivity.kt, temukan fungsi onCreate class MainActivity.
  2. Dalam isi fungsi onCreate, buat objek FastRenderer, lalu teruskan objek viewModel:

MainActivity.kt

class MainActivity : ComponentActivity() {
   ...
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)

      fastRendering = FastRenderer(viewModel)

      lifecycleScope.launch {
      ...
  1. Dalam file yang sama, buat fungsi DrawAreaLowLatency Composable.
  2. Dalam isi fungsi, gunakan AndroidView API untuk menggabungkan tampilan LowLatencySurfaceView, lalu berikan objek fastRendering:

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)
   }
  1. Di fungsi onCreate setelah fungsi Divider Composable, tambahkan fungsi DrawAreaLowLatency 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()
      }
   }
  1. Di direktori gl, buka file LowLatencySurfaceView.kt, lalu perhatikan baris berikut di class LowLatencySurfaceView:
  • Class LowLatencySurfaceView memperluas class SurfaceView. Class tersebut menggunakan metode onTouchListener objek fastRenderer.
  • Antarmuka GLFrontBufferedRenderer.Callback melalui class fastRenderer harus dilampirkan ke objek SurfaceView saat fungsi onAttachedToWindow dipanggil sehingga callback dapat dirender ke tampilan SurfaceView.
  • Antarmuka GLFrontBufferedRenderer.Callback melalui class fastRenderer harus dirilis saat fungsi onDetachedFromWindow 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:

  1. Dalam direktori gl, buka file FastRenderer.kt.
  2. Dalam isi konstanta ACTION_DOWN, buat variabel currentX yang menyimpan koordinat x objek MotionEvent dan variabel currentY yang menyimpan koordinat y-nya.
  3. Buat variabel Segment yang menyimpan objek Segment yang menerima dua instance parameter currentX dan dua instance parameter currentY karena itu merupakan awal baris.
  4. Panggil metode renderFrontBufferedLayer dengan parameter segment untuk memicu callback pada fungsi onDrawFrontBufferedLayer.

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:

  1. Dalam isi konstanta ACTION_MOVE, buat variabel previousX yang menyimpan variabel currentX dan variabel previousY yang menyimpan variabel currentY.
  2. Buat variabel currentX yang menyimpan koordinat x objek MotionEvent saat ini dan variabel currentY yang menyimpan koordinat y saat ini.
  3. Buat variabel Segment yang menyimpan objek Segment yang menerima parameter previousX, previousY, currentX, dan currentY.
  4. Panggil metode renderFrontBufferedLayer dengan parameter segment untuk memicu callback pada fungsi onDrawFrontBufferedLayer 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 konstanta ACTION_UP terdeteksi, panggil metode commit untuk memicu panggilan pada fungsi onDrawDoubleBufferedLayer 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 matriks ModelViewProjection.
  • Fungsi Matrix.multiplyMM melakukan perkalian matriks untuk mengubah data Android ke referensi OpenGL, dan memberikan penyiapan untuk matriks projection.

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:

  1. Dalam file FastRenderer.kt, temukan fungsi callback onDrawFrontBufferedLayer.
  2. Dalam isi fungsi callback onDrawFrontBufferedLayer, panggil fungsi obtainRenderer untuk mendapatkan instance LineRenderer.
  3. Panggil metode drawLine fungsi LineRenderer 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())
}
  1. 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:

  1. Dalam file StylusViewModel.kt, temukan class StylusViewModel, lalu buat variabel openGlLines yang menyimpan daftar objek Segment 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) {
  1. Dalam file FastRenderer.kt, temukan fungsi callback onDrawDoubleBufferedLayer class FastRenderer.
  2. Dalam isi fungsi callback onDrawDoubleBufferedLayer, hapus layar dengan metode GLES20.glClearColor dan GLES20.glClear agar scene dapat dirender dari awal, serta tambahkan baris ke objek viewModel 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())
  1. Buat loop for yang melakukan iterasi dan merender setiap baris dari objek viewModel:

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())
      }
   }
  1. 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:

  1. Dalam file app/build.gradle, impor library di bagian dependensi:

app/build.gradle

...
dependencies {
    ...
    implementation"androidx.input:input-motionprediction:1.0.0-beta01"
  1. Klik File > Sync project with Gradle files.
  2. Di class FastRendering file FastRendering.kt, deklarasikan objek motionEventPredictor sebagai atribut:

FastRenderer.kt

import androidx.input.motionprediction.MotionEventPredictor

class FastRenderer( ... ) {
   ...
   private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
   private var motionEventPredictor: MotionEventPredictor? = null
  1. Dalam fungsi attachSurfaceView, lakukan inisialisasi variabel motionEventPredictor:

FastRenderer.kt

class FastRenderer( ... ) {
   ...
   fun attachSurfaceView(surfaceView: SurfaceView) {
      frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
      motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
   }
  1. Di variabel onTouchListener, panggil metode motionEventPredictor?.record sehingga objek motionEventPredictor 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.

  1. Prediksi objek MotionEvent buatan dengan metode predict.
  2. Buat objek Segment yang menggunakan koordinat x dan y saat ini dan yang diprediksi.
  3. 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.

  1. 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.

Mempelajari lebih lanjut