1. 准备工作
触控笔是一种画笔工具,可帮助用户执行精确的任务。在此 Codelab 中,您将学习如何通过 android.os 和 androidx 库实现自然的触控笔体验。您还将学习如何使用 MotionEvent 类支持压力、倾斜度和方向,并防止手掌误触,以防产生不必要的轻触。此外,您还将学习如何使用 OpenGL 和 SurfaceView 类,通过动作预测和低延迟图形来缩短触控笔延迟时间。
前提条件
- 有使用 Kotlin 和 lambda 的经验。
- 具备有关如何使用 Android Studio 的基础知识。
- 具备有关 Jetpack Compose 的基础知识。
- 对 OpenGL 的低延迟图形有基本的了解。
学习内容
- 如何将
MotionEvent类用于触控笔。 - 如何实现触控笔功能,包括对压力、倾斜度和方向的支持。
- 如何在
Canvas类中实现绘制操作。 - 如何实现动作预测。
- 如何使用 OpenGL 和
SurfaceView类渲染低延迟图形。
所需条件
- 最新版本的 Android Studio。
- 有使用 Kotlin 语法(包括 lambda)的经验。
- 有使用 Compose 的基本经验。如果您不熟悉 Compose,请先完成 Jetpack Compose 基础知识 Codelab。
- 一台支持触控笔的设备。
- 一支主动式触控笔。
- Git。
2. 获取起始代码
如需获取包含起始应用的主题和基本设置的代码,请按以下步骤操作:
- 克隆以下 GitHub 代码库:
git clone https://github.com/android/large-screen-codelabs
- 打开
advanced-stylus文件夹。start文件夹包含起始代码,end文件夹包含解决方案代码。
3. 实现基本绘图应用
首先,为基本绘图应用构建必要的布局,让用户可以进行绘制,并使用 Canvas Composable 函数在界面上显示触控笔属性。该应用如下图所示:

上半部分是一个 Canvas Composable 函数,用于绘制触控笔可视化结果,并显示触控笔的不同属性,如方向、倾斜度和压力。下半部分是另一个 Canvas Composable 函数,用于接收触控笔输入并绘制简单的笔画。
如需实现绘图应用的基本布局,请按以下步骤操作:
- 在 Android Studio 中,打开克隆的代码库。
- 依次点击
app>java>com.example.stylus,然后双击MainActivity。MainActivity.kt文件随即会打开。 - 在
MainActivity类中,请注意观察StylusVisualization和DrawAreaComposable函数。在本部分中,您将重点关注DrawAreaComposable函数。
创建 StylusState 类
- 在同一
ui目录中,依次点击 File > New > Kotlin/Class file。 - 在文本框中,使用
StylusState.kt替换 Name 占位符,然后按Enter(在 macOS 上,按return)。 - 在
StylusState.kt文件中,创建StylusState数据类,然后添加下表中的变量:
变量 | 类型 | 默认值 | 说明 |
|
| 介于 0 到 1.0 之间的值。 | |
|
| 介于 -pi 到 pi 之间的弧度值。 | |
|
| 介于 0 到 pi/2 之间的弧度值。 | |
|
| 存储由 |
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(),
)

- 在
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 类。它包含以下数据:
参数 | 类型 | 说明 |
|
| 坐标 |
|
| 坐标 |
|
| 点的类型 |
DrawPoint 对象有两种类型,由 DrawPointType 枚举描述:
类型 | 说明 |
| 将线条的起点移到某个位置。 |
| 从上一个点跟踪线条。 |
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>()
如需将数据点渲染到路径中,请按以下步骤操作:
- 在
StylusViewModel.kt文件的StylusViewModel类中,添加一个createPath函数。 - 使用
Path()构造函数创建一个类型为Path的path变量。 - 创建一个
for循环,在其中迭代currentPath变量中的每个数据点。 - 如果数据点的类型为
START,请调用moveTo方法,以在指定的x和y坐标处开始绘制一条线。 - 否则,使用数据点的
x和y坐标调用lineTo方法,以连接到上一个数据点。 - 返回
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 对象的一些常量及其数据,它们可用于识别用户在界面上执行的操作:
常量 | 数据 |
| 指针轻触屏幕。它是线条的起点,位于 |
| 指针在界面上移动。它是绘制的线条。 |
| 指针停止轻触界面。线条到此结束。 |
| 检测到不必要的轻触。取消最后一个笔画。 |
当应用收到新的 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 数据类中的更改,请按以下步骤操作:
- 在
StylusViewModel类中,创建类型为MutableStateFlow且类为StylusState的_stylusState变量,以及一个类型为StateFlow且类为StylusState的stylusState变量。每当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
- 创建一个接受
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
}
}
- 在
processMotionEvent函数的末尾,添加带有StylusState参数的requestRendering函数调用。 - 在
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()
)
)
将界面与 StylusViewModel 类相关联
- 在
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 变量。
- 在
DrawAreaComposable函数的正文中,将pointerInteropFilter修饰符添加到CanvasComposable函数以提供MotionEvent对象。
- 将
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)
}
) {
}
}
- 使用
stylusStatepath属性调用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
)
}
}
}
- 运行应用,然后您会发现可以在界面上绘制内容了。
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)
如需详细了解指针和多点触控,请参阅处理多点触控手势。
为压力、方向和倾斜度添加可视化结果
- 在
MainActivity.kt文件中,找到StylusVisualizationComposable函数,然后使用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)
}
}
}
- 运行应用。您会在界面顶部看到三个指示方向、压力和倾斜度的指示器。
- 使用触控笔在界面上涂画,然后观察每个可视化j结果对您的输入如何反应。

- 检查
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_CANCEL 和 FLAG_CANCELED 常量
- 在
StylusViewModel.kt文件中,找到processMotionEvent函数。 - 在
ACTION_UP常量中,创建一个canceled变量,用于检查当前 SDK 版本是否为 Android 13 或更高版本,以及FLAG_CANCELED常量是否已激活。 - 在下一行中,创建一个条件,用于检查
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))
}
}
- 在
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 类。
如要开始使用,请按以下步骤操作:
- 在 Android Studio 中,打开
low-latency文件夹,以获取所有必需的文件: - 请注意项目中的以下新文件:
- 在
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类按如下方式定义线段:x1、y1:第一个点的坐标x2、y2:第二个点的坐标
下图显示了数据如何在每个类之间移动:

创建低延迟界面和布局
- 在
MainActivity.kt文件中,找到MainActivity类的onCreate函数。 - 在
onCreate函数的正文中,创建一个FastRenderer对象,然后传入viewModel对象:
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fastRendering = FastRenderer(viewModel)
lifecycleScope.launch {
...
- 在同一文件中,创建一个
DrawAreaLowLatencyComposable函数。 - 在函数正文中,使用
AndroidViewAPI 封装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)
}
- 在
onCreate函数中的DividerComposable函数后面,将DrawAreaLowLatencyComposable函数添加到布局中:
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()
}
}
- 在
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 对象,请按以下步骤操作:
- 打开
gl目录下的FastRenderer.kt文件。 - 在
ACTION_DOWN常量的正文中,创建用于存储MotionEvent对象的x坐标的currentX变量和存储其y坐标的currentY变量。 - 创建一个
Segment变量,用于存储Segment对象,该对象接受currentX参数的两个实例和currentY参数的两个实例,因为它是线条的起点。 - 使用
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 对象,请按以下步骤操作:
- 在
ACTION_MOVE常量的正文中,创建用于存储currentX变量的previousX变量和存储currentY变量的previousY变量。 - 创建一个用于保存
MotionEvent对象当前x坐标的currentX变量,以及一个用于保存其当前y坐标的currentY变量。 - 创建一个
Segment变量,用于存储接受previousX、previousY、currentX和currentY参数的Segment对象。 - 使用
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 文件中,onDrawFrontBufferedLayer 和 onDrawDoubleBufferedLayer 回调函数会执行 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 回调函数,请按以下步骤操作:
- 在
FastRenderer.kt文件中,找到onDrawFrontBufferedLayer回调函数。 - 在
onDrawFrontBufferedLayer回调函数的正文中,调用obtainRenderer函数以获取LineRenderer实例。 - 使用以下参数调用
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())
}
- 运行应用,然后您会发现在界面上绘制时的延迟时间非常短。不过,应用不会保留该线条,因为您仍需实现
onDrawDoubleBufferedLayer回调函数。
系统会在 commit 函数之后调用 onDrawDoubleBufferedLayer 回调函数,以便保留该线条。回调会提供 params 值,其中包含一系列 Segment 对象。前端缓冲区上的所有线段都会在双缓冲区中重放以持久保留。
如需实现 onDrawDoubleBufferedLayer 回调函数,请按以下步骤操作:
- 在
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) {
- 在
FastRenderer.kt文件中,找到FastRenderer类的onDrawDoubleBufferedLayer回调函数。 - 在
onDrawDoubleBufferedLayer回调函数的正文中,使用GLES20.glClearColor和GLES20.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())
- 创建一个
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())
}
}
- 运行应用,然后您会发现您可以在界面上绘制内容;线条在触发
ACTION_UP常量后会保留。
7. 实现动作预测
您可以使用 androidx.input 库进一步缩短延迟时间。该库可分析触控笔的方向,并预测下一个点的位置,然后插入该点以进行渲染。
如需设置动作预测,请按以下步骤操作:
- 在
app/build.gradle文件中,在依赖项部分中导入该库:
app/build.gradle
...
dependencies {
...
implementation"androidx.input:input-motionprediction:1.0.0-beta01"
- 依次点击 File > Sync project with Gradle files。
- 在
FastRendering.kt文件的FastRendering类中,将motionEventPredictor对象声明为属性:
FastRenderer.kt
import androidx.input.motionprediction.MotionEventPredictor
class FastRenderer( ... ) {
...
private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
private var motionEventPredictor: MotionEventPredictor? = null
- 在
attachSurfaceView函数中,初始化motionEventPredictor变量:
FastRenderer.kt
class FastRenderer( ... ) {
...
fun attachSurfaceView(surfaceView: SurfaceView) {
frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
}
- 在
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 对象记录的时间。换言之,您应预测笔画的发生时间。
- 使用
predict方法预测人工MotionEvent对象。 - 创建一个使用当前及预测的 x 坐标和 y 坐标的
Segment对象。 - 使用
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)
}
}
...
}
插入预测事件以进行渲染,从而缩短延迟时间。
- 运行应用,然后注意延迟时间是否缩短。
缩短延迟时间将为触控笔用户提供更自然的触控笔体验。
8. 恭喜
恭喜!您已了解了如何像专业人士一样处理触控笔!
您学习了如何处理 MotionEvent 对象,以提取有关压力、方向和倾斜度的信息。您还学习了如何通过实现 androidx.graphics 库和 androidx.input 库来缩短延迟时间。这些增强功能相辅相成,提供更自然的触控笔体验。