增强 Android 应用中的触控笔性能支持

1. 准备工作

触控笔是一种画笔工具,可帮助用户执行精确的任务。在此 Codelab 中,您将学习如何通过 android.osandroidx 库实现自然的触控笔体验。您还将学习如何使用 MotionEvent 类支持压力、倾斜度和方向,并防止手掌误触,以防产生不必要的轻触。此外,您还将学习如何使用 OpenGL 和 SurfaceView 类,通过动作预测和低延迟图形来缩短触控笔延迟时间。

前提条件

  • 有使用 Kotlin 和 lambda 的经验。
  • 具备有关如何使用 Android Studio 的基础知识。
  • 具备有关 Jetpack Compose 的基础知识。
  • 对 OpenGL 的低延迟图形有基本的了解。

学习内容

  • 如何将 MotionEvent 类用于触控笔。
  • 如何实现触控笔功能,包括对压力、倾斜度和方向的支持。
  • 如何在 Canvas 类中实现绘制操作。
  • 如何实现动作预测。
  • 如何使用 OpenGL 和 SurfaceView 类渲染低延迟图形。

所需条件

2. 获取起始代码

如需获取包含起始应用的主题和基本设置的代码,请按以下步骤操作:

  1. 克隆以下 GitHub 代码库:
git clone https://github.com/android/large-screen-codelabs
  1. 打开 advanced-stylus 文件夹。start 文件夹包含起始代码,end 文件夹包含解决方案代码。

3. 实现基本绘图应用

首先,为基本绘图应用构建必要的布局,让用户可以进行绘制,并使用 Canvas Composable 函数在界面上显示触控笔属性。该应用如下图所示:

基本的绘图应用。上半部分用于绘制可视化结果,下半部分用于绘图。

上半部分是一个 Canvas Composable 函数,用于绘制触控笔可视化结果,并显示触控笔的不同属性,如方向、倾斜度和压力。下半部分是另一个 Canvas Composable 函数,用于接收触控笔输入并绘制简单的笔画。

如需实现绘图应用的基本布局,请按以下步骤操作:

  1. 在 Android Studio 中,打开克隆的代码库。
  2. 依次点击 app > java > com.example.stylus,然后双击 MainActivityMainActivity.kt 文件随即会打开。
  3. MainActivity 类中,请注意观察 StylusVisualizationDrawArea Composable 函数。在本部分中,您将重点关注 DrawArea Composable 函数。

创建 StylusState

  1. 在同一 ui 目录中,依次点击 File > New > Kotlin/Class file
  2. 在文本框中,使用 StylusState.kt 替换 Name 占位符,然后按 Enter(在 macOS 上,按 return)。
  3. StylusState.kt 文件中,创建 StylusState 数据类,然后添加下表中的变量:

变量

类型

默认值

说明

pressure

Float

0F

介于 0 到 1.0 之间的值。

orientation

Float

0F

介于 -pi 到 pi 之间的弧度值。

tilt

Float

0F

介于 0 到 pi/2 之间的弧度值。

path

Path

Path()

存储由 Canvas Composable 函数通过 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(),
)

显示方向、倾斜度和压力指标的信息中心视图

  1. MainActivity.kt 文件中,找到 MainActivity 类,然后使用 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())

DrawPoint

DrawPoint 类会存储有关在界面上绘制的每个点的数据;当您将这些点连接起来后,便可创建线条。它模拟了 Path 对象的运作方式。

DrawPoint 类扩展了 PointF 类。它包含以下数据:

参数

类型

说明

x

Float

坐标

y

Float

坐标

type

DrawPointType

点的类型

DrawPoint 对象有两种类型,由 DrawPointType 枚举描述:

类型

说明

START

将线条的起点移到某个位置。

LINE

从上一个点跟踪线条。

DrawPoint.kt

import android.graphics.PointF
class DrawPoint(x: Float, y: Float, val type: DrawPointType): PointF(x, y)

将数据点渲染到路径中

对于此应用,StylusViewModel 类会保留线条数据,准备数据以进行渲染,并对 Path 对象执行一些操作以便防止手掌误触。

  • 如需保留线条的数据,请在 StylusViewModel 类中创建一个 DrawPoint 对象的可变列表:

StylusViewModel.kt

import androidx.lifecycle.ViewModel
import com.example.stylus.data.DrawPoint

class StylusViewModel : ViewModel() {private var currentPath = mutableListOf<DrawPoint>()

如需将数据点渲染到路径中,请按以下步骤操作:

  1. StylusViewModel.kt 文件的 StylusViewModel 类中,添加一个 createPath 函数。
  2. 使用 Path() 构造函数创建一个类型为 Pathpath 变量。
  3. 创建一个 for 循环,在其中迭代 currentPath 变量中的每个数据点。
  4. 如果数据点的类型为 START,请调用 moveTo 方法,以在指定的 xy 坐标处开始绘制一条线。
  5. 否则,使用数据点的 xy 坐标调用 lineTo 方法,以连接到上一个数据点。
  6. 返回 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() {
}

处理 MotionEvent 对象

触控笔事件会经历一些 MotionEvent 对象,这些对象提供有关所执行操作的信息以及关联数据,例如指针的位置和压力。下表列出了 MotionEvent 对象的一些常量及其数据,它们可用于识别用户在界面上执行的操作:

常量

数据

ACTION_DOWN

指针轻触屏幕。它是线条的起点,位于 MotionEvent 对象报告的位置。

ACTION_MOVE

指针在界面上移动。它是绘制的线条。

ACTION_UP

指针停止轻触界面。线条到此结束。

ACTION_CANCEL

检测到不必要的轻触。取消最后一个笔画。

当应用收到新的 MotionEvent 对象时,界面应该会进行渲染以反映新的用户输入。

  • 若要处理 StylusViewModel 类中的 MotionEvent 对象,请创建一个收集线条坐标的函数:

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
   }

将数据发送到界面

如需更新 StylusViewModel 类,以便界面可以收集 StylusState 数据类中的更改,请按以下步骤操作:

  1. StylusViewModel 类中,创建类型为 MutableStateFlow 且类为 StylusState_stylusState 变量,以及一个类型为 StateFlow 且类为 StylusStatestylusState 变量。每当 StylusViewModel 类中的触控笔状态发生变化,并且 MainActivity 类中的界面使用了 stylusState 变量时,_stylusState 变量都会修改。

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. 创建一个接受 StylusState 对象参数的 requestRendering 函数:

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. processMotionEvent 函数的末尾,添加带有 StylusState 参数的 requestRendering 函数调用。
  2. StylusState 参数中,从 motionEvent 变量检索倾斜度、压力和方向值,然后使用 createPath() 函数创建路径。这会触发数据流事件,稍后您可以在界面中连接该事件。

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. MainActivity 类中,找到 onCreate 函数的 super.onCreate 函数,然后添加状态收集方式。如需详细了解状态收集方式,请参阅以可感知生命周期的方式收集数据流

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

现在,每当 StylusViewModel 类发布新的 StylusState 状态时,activity 都会收到该状态,并且新的 StylusState 对象会更新本地 MainActivity 类的 stylusState 变量。

  1. DrawArea Composable 函数的正文中,将 pointerInteropFilter 修饰符添加到 Canvas Composable 函数以提供 MotionEvent 对象。
  1. MotionEvent 对象发送到 StylusViewModel 的 processMotionEvent 函数进行处理:

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. 使用 stylusState path 属性调用 drawPath 函数,然后提供颜色和笔画样式。

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. 运行应用,然后您会发现可以在界面上绘制内容了。

4. 实现对压力、方向和倾斜度的支持

在上一部分中,您了解了如何从 MotionEvent 对象中检索触控笔信息,例如压力、方向和倾斜度。

StylusViewModel.kt

tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,

不过,此快捷方式仅适用于第一个指针。检测到多点触控时,会检测到多个指针,而此快捷方式仅返回第一个指针(即界面上的第一个指针)的值。如需请求有关特定指针的数据,您可以使用 pointerIndex 参数:

StylusViewModel.kt

tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex),
pressure = motionEvent.getPressure(pointerIndex),
orientation = motionEvent.getOrientation(pointerIndex)

如需详细了解指针和多点触控,请参阅处理多点触控手势

为压力、方向和倾斜度添加可视化结果

  1. MainActivity.kt 文件中,找到 StylusVisualization Composable 函数,然后使用 StylusState 数据流对象的信息来渲染可视化结果:

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. 运行应用。您会在界面顶部看到三个指示方向、压力和倾斜度的指示器。
  2. 使用触控笔在界面上涂画,然后观察每个可视化j结果对您的输入如何反应。

针对使用触控笔手写的“hello”一词显示的方向、压力和倾斜度

  1. 检查 StylusVisualization.kt 文件,了解如何构建每个可视化结果。

5. 实现防手掌误触功能

界面可以注册不必要的轻触。例如,为了支撑身体,用户在手写时会自然地将手放在屏幕上。

防手掌误触是一种用于检测此行为的机制,它会通知开发者取消上一组 MotionEvent 对象。一组以 ACTION_DOWN 常量开头的 MotionEvent 对象。

也就是说,您必须保留输入内容的历史记录,以便可以从界面上移除多余的轻触,然后重新渲染有效的用户输入。幸运的是,您已将历史记录存储在 StylusViewModel 类的 currentPath 变量中。

Android 提供 MotionEvent 对象中的 ACTION_CANCEL 常量,以通知开发者不必要的轻触。从 Android 13 开始,MotionEvent 对象会提供应针对 ACTION_POINTER_UP 常量进行检查的 FLAG_CANCELED 常量。

实现 cancelLastStroke 函数

  • 若要从最后一个 START 数据点移除数据点,请返回到 StylusViewModel 类,然后创建一个 cancelLastStroke 函数,该函数查找最后一个 START 数据点的索引,并且仅保留第一个数据点中的数据,直到索引减 1:

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)
      }
   }

添加 ACTION_CANCELFLAG_CANCELED 常量

  1. StylusViewModel.kt 文件中,找到 processMotionEvent 函数。
  2. ACTION_UP 常量中,创建一个 canceled 变量,用于检查当前 SDK 版本是否为 Android 13 或更高版本,以及 FLAG_CANCELED 常量是否已激活。
  3. 在下一行中,创建一个条件,用于检查 canceled 变量是否为 true。如果是,则调用 cancelLastStroke 函数来移除上一组 MotionEvent 对象。如果不是,则调用 currentPath.add 方法来添加上一组 MotionEvent 对象。

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. ACTION_CANCEL 常量中,请注意观察 cancelLastStroke 函数:

StylusViewModel.kt

...
class StylusViewModel : ViewModel() {
    ...
    fun processMotionEvent(motionEvent: MotionEvent): Boolean {
        ...
        MotionEvent.ACTION_CANCEL -> {
           // unwanted touch detected
           cancelLastStroke()
        }

已实现防止手掌误触!您可以在 palm-rejection 文件夹中找到有效的代码。

6. 实现低延迟

在本部分中,您将缩短用户输入内容与界面渲染之间的延迟时间,以提高性能。导致出现延迟时间的原因有多种,其中之一是长图形管道。您可以通过前端缓冲区渲染来减小图形管道。借助前端缓冲区渲染功能,开发者可以直接访问界面缓冲区,从而获得出色的手写和素描效果。

androidx.graphics提供的 GLFrontBufferedRenderer 类负责前端和双倍缓冲区渲染。它使用 onDrawFrontBufferedLayer 回调函数优化 SurfaceView 对象,以实现快速渲染,并使用 onDrawDoubleBufferedLayer 回调函数进行正常渲染。GLFrontBufferedRenderer 类和 GLFrontBufferedRenderer.Callback 接口可处理用户提供的数据类型。在此 Codelab 中,您将使用 Segment 类。

如要开始使用,请按以下步骤操作:

  1. 在 Android Studio 中,打开 low-latency 文件夹,以获取所有必需的文件:
  2. 请注意项目中的以下新文件:
  • build.gradle 文件中,androidx.graphics已通过 implementation "androidx.graphics:graphics-core:1.0.0-alpha03" 声明导入。
  • LowLatencySurfaceView 类会扩展 SurfaceView 类,以在界面上渲染 OpenGL 代码。
  • LineRenderer 类包含 OpenGL 代码,用于在界面上渲染线条。
  • FastRenderer 类支持快速渲染,并实现了 GLFrontBufferedRenderer.Callback 接口。它还可拦截 MotionEvent 对象。
  • StylusViewModel 类包含数据点以及 LineManager 接口。
  • Segment 类按如下方式定义线段:
  • x1y1:第一个点的坐标
  • x2y2:第二个点的坐标

下图显示了数据如何在每个类之间移动:

MotionEvent 由 LowLatencySurfaceView 捕获,并发送到 onTouchListener 进行处理。onTouchListener 进行处理,并请求 GLFrontBufferRenderer 进行前端或双倍缓冲区渲染。GLFrontBufferRenderer 渲染到 LowLatencySurfaceView。

创建低延迟界面和布局

  1. MainActivity.kt 文件中,找到 MainActivity 类的 onCreate 函数。
  2. onCreate 函数的正文中,创建一个 FastRenderer 对象,然后传入 viewModel 对象:

MainActivity.kt

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

      fastRendering = FastRenderer(viewModel)

      lifecycleScope.launch {
      ...
  1. 在同一文件中,创建一个 DrawAreaLowLatency Composable 函数。
  2. 在函数正文中,使用 AndroidView API 封装 LowLatencySurfaceView 视图,然后提供 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. onCreate 函数中的 Divider Composable 函数后面,将 DrawAreaLowLatency Composable 函数添加到布局中:

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. gl 目录中,打开 LowLatencySurfaceView.kt 文件,然后注意 LowLatencySurfaceView 类中的以下内容:
  • LowLatencySurfaceView 类扩展了 SurfaceView 类。它会使用 fastRenderer 对象的 onTouchListener 方法。
  • 调用 onAttachedToWindow 函数时,需要将使用 fastRenderer 类的 GLFrontBufferedRenderer.Callback 接口附加到 SurfaceView 对象,以便回调可以渲染到 SurfaceView 视图。
  • 调用 onDetachedFromWindow 函数时,需要释放使用 fastRenderer 类的 GLFrontBufferedRenderer.Callback 接口。

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

使用 onTouchListener 接口处理 MotionEvent 对象

如需在检测到 ACTION_DOWN 常量时处理 MotionEvent 对象,请按以下步骤操作:

  1. 打开 gl 目录下的 FastRenderer.kt 文件。
  2. ACTION_DOWN 常量的正文中,创建用于存储 MotionEvent 对象的 x 坐标的 currentX 变量和存储其 y 坐标的 currentY 变量。
  3. 创建一个 Segment 变量,用于存储 Segment 对象,该对象接受 currentX 参数的两个实例和 currentY 参数的两个实例,因为它是线条的起点。
  4. 使用 segment 参数调用 renderFrontBufferedLayer 方法,以触发对 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)
   }

如需在检测到 ACTION_MOVE 常量时处理 MotionEvent 对象,请按以下步骤操作:

  1. ACTION_MOVE 常量的正文中,创建用于存储 currentX 变量的 previousX 变量和存储 currentY 变量的 previousY 变量。
  2. 创建一个用于保存 MotionEvent 对象当前 x 坐标的 currentX 变量,以及一个用于保存其当前 y 坐标的 currentY 变量。
  3. 创建一个 Segment 变量,用于存储接受 previousXpreviousYcurrentXcurrentY 参数的 Segment 对象。
  4. 使用 segment 参数调用 renderFrontBufferedLayer 方法,以触发对 onDrawFrontBufferedLayer 函数的回调并执行 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)
   }
  • 如需在检测到 ACTION_UP 常量时处理 MotionEvent 对象,请调用 commit 方法以触发对 onDrawDoubleBufferedLayer 函数的调用并执行 OpenGL 代码:

FastRenderer.kt

class FastRenderer ( ... ) {
   ...
   val onTouchListener = View.OnTouchListener { view, event ->
   ...
   MotionEvent.ACTION_UP -> {
      frontBufferRenderer?.commit()
   }

实现 GLFrontBufferedRenderer 回调函数

FastRenderer.kt 文件中,onDrawFrontBufferedLayeronDrawDoubleBufferedLayer 回调函数会执行 OpenGL 代码。在每个回调函数的开头,以下 OpenGL 函数会将 Android 数据映射到 OpenGL 工作区:

  • GLES20.glViewport 函数可定义在其中渲染场景的矩形的大小。
  • Matrix.orthoM 函数会计算 ModelViewProjection 矩阵。
  • Matrix.multiplyMM 函数会执行矩阵乘法,以将 Android 数据转换为 OpenGL 引用,并为 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)

系统会为您完成这部分代码设置,以便您专注于执行实际渲染的代码。onDrawFrontBufferedLayer 回调函数用于渲染界面上的小区域。它可提供 Segment 类型的 param 值,以便您快速渲染单个线段。LineRenderer 类是笔刷的 OpenGL 渲染程序,会应用线条的颜色和大小。

如需实现 onDrawFrontBufferedLayer 回调函数,请按以下步骤操作:

  1. FastRenderer.kt 文件中,找到 onDrawFrontBufferedLayer 回调函数。
  2. onDrawFrontBufferedLayer 回调函数的正文中,调用 obtainRenderer 函数以获取 LineRenderer 实例。
  3. 使用以下参数调用 LineRenderer 函数的 drawLine 方法:
  • 之前计算的 projection 矩阵。
  • Segment 对象的列表,在本例中为单个线段。
  • 线条的 color

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. 运行应用,然后您会发现在界面上绘制时的延迟时间非常短。不过,应用不会保留该线条,因为您仍需实现 onDrawDoubleBufferedLayer 回调函数。

系统会在 commit 函数之后调用 onDrawDoubleBufferedLayer 回调函数,以便保留该线条。回调会提供 params 值,其中包含一系列 Segment 对象。前端缓冲区上的所有线段都会在双缓冲区中重放以持久保留。

如需实现 onDrawDoubleBufferedLayer 回调函数,请按以下步骤操作:

  1. StylusViewModel.kt 文件中,找到 StylusViewModel 类,然后创建一个 openGlLines 变量来存储 Segment 对象的可变列表:

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. FastRenderer.kt 文件中,找到 FastRenderer 类的 onDrawDoubleBufferedLayer 回调函数。
  2. onDrawDoubleBufferedLayer 回调函数的正文中,使用 GLES20.glClearColorGLES20.glClear 方法清除界面,以便从头开始渲染场景,并将线条添加到 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())
  1. 创建一个 for 循环,用于迭代并渲染 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. 运行应用,然后您会发现您可以在界面上绘制内容;线条在触发 ACTION_UP 常量后会保留。

7. 实现动作预测

您可以使用 androidx.input进一步缩短延迟时间。该库可分析触控笔的方向,并预测下一个点的位置,然后插入该点以进行渲染。

如需设置动作预测,请按以下步骤操作:

  1. app/build.gradle 文件中,在依赖项部分中导入该库:

app/build.gradle

...
dependencies {
    ...
    implementation"androidx.input:input-motionprediction:1.0.0-beta01"
  1. 依次点击 File > Sync project with Gradle files
  2. FastRendering.kt 文件的 FastRendering 类中,将 motionEventPredictor 对象声明为属性:

FastRenderer.kt

import androidx.input.motionprediction.MotionEventPredictor

class FastRenderer( ... ) {
   ...
   private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
   private var motionEventPredictor: MotionEventPredictor? = null
  1. attachSurfaceView 函数中,初始化 motionEventPredictor 变量:

FastRenderer.kt

class FastRenderer( ... ) {
   ...
   fun attachSurfaceView(surfaceView: SurfaceView) {
      frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
      motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
   }
  1. onTouchListener 变量中,调用 motionEventPredictor?.record 方法,以便 motionEventPredictor 对象获取运动数据:

FastRendering.kt

class FastRenderer( ... ) {
   ...
   val onTouchListener = View.OnTouchListener { view, event ->
      motionEventPredictor?.record(event)
      ...
      when (event?.action) {

下一步是使用 predict 函数预测 MotionEvent 对象。我们建议预测收到 ACTION_MOVE 常量的时间以及完成 MotionEvent 对象记录的时间。换言之,您应预测笔画的发生时间。

  1. 使用 predict 方法预测人工 MotionEvent 对象。
  2. 创建一个使用当前及预测的 x 坐标和 y 坐标的 Segment 对象。
  3. 使用 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)
              }

          }
          ...
       }

插入预测事件以进行渲染,从而缩短延迟时间。

  1. 运行应用,然后注意延迟时间是否缩短。

缩短延迟时间将为触控笔用户提供更自然的触控笔体验。

8. 恭喜

恭喜!您已了解了如何像专业人士一样处理触控笔!

您学习了如何处理 MotionEvent 对象,以提取有关压力、方向和倾斜度的信息。您还学习了如何通过实现 androidx.graphicsandroidx.input来缩短延迟时间。这些增强功能相辅相成,提供更自然的触控笔体验。

了解详情