Android 和 ChromeOS 提供了多种 API,可帮助您构建可为用户提供卓越触控笔体验的应用。MotionEvent
类可显示有关触控笔与屏幕交互的信息,包括触控笔压力、方向、倾斜度、悬停距离和手掌检测信息。低延迟图形和动作预测库可增强触控笔在屏幕上的渲染效果,提供自然的如笔画般的体验。
MotionEvent
MotionEvent
类表示用户输入交互,例如触摸指针在屏幕上的位置和移动。对于触控笔输入,MotionEvent
还会公开压力、方向、倾斜度和悬停数据。
事件数据
如需访问 MotionEvent
数据,请为组件添加 pointerInput
修饰符:
@Composable
fun Greeting() {
Text(
text = "Hello, Android!", textAlign = TextAlign.Center, style = TextStyle(fontSize = 5.em),
modifier = Modifier
.pointerInput(Unit) {
awaitEachGesture {
while (true) {
val event = awaitPointerEvent()
event.changes.forEach { println(it) }
}
}
},
)
}
MotionEvent
对象提供与界面事件以下几方面相关的数据:
- 操作:与设备进行物理交互,例如轻触屏幕、在屏幕表面上移动指针、将指针悬停在屏幕表面上
- 指针:与屏幕互动的对象(手指、触控笔、鼠标)的标识符
- 轴:数据类型 - x 和 y 坐标、压力、倾斜度、方向和悬停数据(距离)
操作
如需实现触控笔支持,您需要了解用户正在执行的操作。
MotionEvent
提供了各种用于定义动作事件的 ACTION
常量。触控笔最重要的操作如下:
操作 | 说明 |
---|---|
ACTION_DOWN ACTION_POINTER_DOWN |
指针已与屏幕接触。 |
ACTION_MOVE | 指针正在屏幕上移动。 |
ACTION_UP ACTION_POINTER_UP |
指针与屏幕断开接触 |
ACTION_CANCEL | 此时应取消之前或当前的动作集。 |
应用可以执行诸如以下任务:在发生 ACTION_DOWN
时开始新的描边、使用 ACTION_MOVE,
绘制描边,以及在触发 ACTION_UP
时完成描边。
对于给定指针,从 ACTION_DOWN
到 ACTION_UP
的一系列 MotionEvent
操作称为动作集。
指针
大多数屏幕都是多点触控的:系统会为每个手指、触控笔、鼠标或其他与屏幕互动的指向对象分配一个指针。借助触控点索引,您可以获取特定指针的轴信息,例如第一个手指或第二个手指轻触屏幕的位置。
指针索引的范围为 0 到 MotionEvent#pointerCount()
返回的指针数减 1。
您可以使用 getAxisValue(axis,
pointerIndex)
方法访问指针的轴值。如果省略指针索引,系统会返回第一个指针(即指针零 (0))的值。
MotionEvent
对象包含所用指针类型的相关信息。您可以通过迭代触控点索引并调用 getToolType(pointerIndex)
方法来获取指针类型。
如需详细了解指针,请参阅处理多点触控手势。
触控笔输入
您可以使用 TOOL_TYPE_STYLUS
过滤触控笔输入:
val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)
触控笔还可以通过 TOOL_TYPE_ERASER
报告将其用作橡皮擦:
val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)
触控笔轴数据
ACTION_DOWN
和 ACTION_MOVE
提供触控笔的轴数据,即 x 和 y 坐标、压力、方向、倾斜度和悬停距离。
为了能够访问这些数据,MotionEvent
API 提供了 getAxisValue(int)
,其中参数是以下任何轴标识符:
轴 | getAxisValue() 的返回值 |
---|---|
AXIS_X |
动作事件的 X 坐标。 |
AXIS_Y |
动作事件的 Y 坐标。 |
AXIS_PRESSURE |
对于触摸屏或触控板,值为手指、触控笔或其他指针施加的压力。对于鼠标或轨迹球,如果按下主按钮,则值为 1,否则为 0。 |
AXIS_ORIENTATION |
对于触摸屏或触控板,值为手指、触控笔或其他指针相对于设备垂直面的方向。 |
AXIS_TILT |
触控笔的倾斜度(以弧度为单位)。 |
AXIS_DISTANCE |
触控笔与屏幕之间的距离。 |
例如,MotionEvent.getAxisValue(AXIS_X)
会返回第一个指针的 x 坐标。
另请参阅处理多点触控手势。
位置
您可以调用以下方法来检索指针的 x 坐标和 y 坐标:
MotionEvent#getAxisValue(AXIS_X)
或MotionEvent#getX()
MotionEvent#getAxisValue(AXIS_Y)
或MotionEvent#getY()
压力
您可以使用 MotionEvent#getAxisValue(AXIS_PRESSURE)
检索指针压力;对于第一个指针,您可以使用 MotionEvent#getPressure()
检索指针压力。
触摸屏或触控板的压力值介于 0(无压力)到 1 之间,但可以根据屏幕校准返回更高的值。
方向
方向表示触控笔所指方向。
您可以使用 getAxisValue(AXIS_ORIENTATION)
或 getOrientation()
(针对第一个指针)检索指针方向。
对于触控笔,返回的方向值为顺时针方向为 0 到 pi (π) 或 0 到 -pi 之间的弧度值。
有了方向,您可以实现逼真的画笔体验。例如,如果触控笔表示扁平画笔,则扁平画笔的宽度取决于触控笔方向。
倾斜度
倾斜度是指触控笔相对于屏幕的倾斜度。
倾斜度会返回触控笔的正角度(以弧度为单位),其中 0 表示垂直于屏幕,而 π/2 表示与屏幕平行。
可以使用 getAxisValue(AXIS_TILT)
(第一个指针没有快捷方式)检索倾斜角度。
倾斜度可用于重现尽可能贴近真实工具,例如用倾斜的铅笔模仿阴影。
悬停
触控笔与屏幕之间的距离可通过 getAxisValue(AXIS_DISTANCE)
获取。当触控笔远离屏幕时,该方法会返回 0.0(与屏幕接触)到更高的值。屏幕与触控笔头(笔尖)之间的悬停距离取决于屏幕和触控笔的制造商。由于实现方式可能会有所不同,因此请勿依赖应用关键型功能的精确值。
触控笔悬停可用于预览画笔的大小,或指示将要选择某个按钮。
注意:Compose 提供的修饰符会影响界面元素的交互状态:
hoverable
:将组件配置为可通过指针进入和退出事件悬停。indication
:在发生互动时为此组件绘制视觉效果。
防手掌误触、导航和不必要的输入
有时,多点触控屏幕可能会注册不必要的轻触,例如,为了支撑身体,用户在手写时会自然地将手放在屏幕上。防手掌误触是一种用于检测此行为的机制,会通知您应取消最后一组 MotionEvent
集。
因此,您必须保留用户输入的历史记录,以便从屏幕上移除不必要的轻触,并重新渲染合法的用户输入。
ACTION_CANCEL 和 FLAG_CANCELED
ACTION_CANCEL
和 FLAG_CANCELED
都旨在通知您,应从最后一个 ACTION_DOWN
开始取消之前的 MotionEvent
集,例如,您可以撤消绘图应用的给定指针的最后一笔。
ACTION_CANCEL
在 Android 1.0(API 级别 1)中添加
ACTION_CANCEL
表示应取消上一组动作事件。
当系统检测到以下任一情况时,就会触发 ACTION_CANCEL
:
- 导航手势
- 防止手掌误触
触发 ACTION_CANCEL
时,您应使用 getPointerId(getActionIndex())
标识活动指针。然后,从输入历史记录中移除使用该指针创建的笔触,并重新渲染场景。
FLAG_CANCELED
在 Android 13(API 级别 33)中添加
FLAG_CANCELED
表示指针上移是因用户无意间轻触所致。该标志通常是在用户不小心轻触屏幕时设置的,例如抓握设备或将手掌放在屏幕上时。
您可以按如下方式访问标志值:
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
如果设置了此标志,则需要从此指针的最后一次 ACTION_DOWN
操作中撤消最后一组 MotionEvent
集。
与 ACTION_CANCEL
一样,可以通过 getPointerId(actionIndex)
找到指针。
全屏、无边框和导航手势
如果应用全屏显示且在边缘附近有可操作元素,例如绘图或记事应用的画布,则从屏幕底部滑动以显示导航或将应用移至后台,可能会导致画布上出现不必要的轻触。
为了防止手势在应用中触发不必要的轻触,您可以利用边衬区和 ACTION_CANCEL
。
另请参阅防手掌误触、导航和不必要的输入部分。
使用 setSystemBarsBehavior()
方法和 WindowInsetsController
的 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
可防止导航手势导致不必要的轻触事件:
// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
如需详细了解边衬区和手势管理,请参阅:
低延迟时间
延迟时间是指硬件、系统和应用处理和渲染用户输入所需的时间。
延迟时间 = 硬件和操作系统输入处理 + 应用处理 + 系统合成
- 硬件渲染
延迟来源
- 在触摸屏(硬件)上注册触控笔:当触控笔和操作系统通信以注册和同步时,进行初始无线连接。
- 触摸采样率(硬件):触摸屏每秒检查指针是否轻触屏幕表面的次数,范围从 60 Hz 到 1000 Hz。
- 输入处理(应用):对用户输入应用颜色、图形效果和转换功能。
- 图形渲染(操作系统 + 硬件):缓冲区交换、硬件处理。
低延迟图形
Jetpack 低延迟图形库可以缩短用户输入与屏幕渲染之间的处理时间。
该库可避免多缓冲区渲染,并利用前端缓冲区渲染技术(即直接写入屏幕),从而缩短处理时间。
前端缓冲区渲染
前端缓冲区是用于屏幕渲染的内存区。它是最接近应用直接绘制到屏幕的途径。借助低延迟库,应用可以直接渲染到前端缓冲区。这样可以防止缓冲区交换,从而提高性能,常规多缓冲区渲染或双缓冲区渲染(最常见的情况)可能会发生缓冲区交换。
虽然前端缓冲区渲染是渲染屏幕小区域的绝佳技术,但并不适用于刷新整个屏幕。使用前端缓冲区渲染时,应用会将内容渲染到显示屏正在读取数据的缓冲区中。因此,可能会出现渲染伪影或画面撕裂 现象(见下文)。
低延迟库适用于 Android 10(API 级别 29)及更高版本,以及搭载 Android 10(API 级别 29)及更高版本的 ChromeOS 设备。
依赖项
低延迟库为前端缓冲区渲染实现提供了组件。将该库作为依赖项添加到应用的模块 build.gradle
文件中:
dependencies {
implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}
GLFrontBufferRenderer 回调
低延迟库包含 GLFrontBufferRenderer.Callback
接口,该接口定义了以下方法:
低延迟库并不判断用于 GLFrontBufferRenderer
的数据类型,
不过,该库会将数据处理为包含数百个数据点的数据流;因此,请设计数据以优化内存使用和分配。
回调
如需启用渲染回调,请实现 GLFrontBufferedRenderer.Callback
并替换 onDrawFrontBufferedLayer()
和 onDrawDoubleBufferedLayer()
。GLFrontBufferedRenderer
使用回调以尽可能最优化的方式渲染数据。
val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> {
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
param: DATA_TYPE
) {
// OpenGL for front buffer, short, affecting small area of the screen.
}
override fun onDrawMultiDoubleBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<DATA_TYPE>
) {
// OpenGL full scene rendering.
}
}
声明一个 GLFrontBufferedRenderer 实例
通过提供您之前创建的 SurfaceView
和回调来准备 GLFrontBufferedRenderer
。GLFrontBufferedRenderer
会使用回调优化对前端缓冲区和双缓冲区的渲染:
var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
渲染
当您调用 renderFrontBufferedLayer()
方法时,前端缓冲区渲染开始,该方法会触发 onDrawFrontBufferedLayer()
回调。
当您调用 commit()
函数时,会触发 onDrawMultiDoubleBufferedLayer()
回调,从而恢复双缓冲区渲染。
在以下示例中,当用户开始在屏幕上绘图 (ACTION_DOWN
) 并移动指针 (ACTION_MOVE
) 时,该进程会渲染到前端缓冲区(快速渲染)。当指针离开屏幕表面 (ACTION_UP
) 时,该进程会渲染到双缓冲区。
您可以使用 requestUnbufferedDispatch()
要求输入系统不要批量处理动作事件,而是在可用时立即传送这些事件:
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> {
// Deliver input events as soon as they arrive.
view.requestUnbufferedDispatch(motionEvent)
// Pointer is in contact with the screen.
glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
}
MotionEvent.ACTION_MOVE -> {
// Pointer is moving.
glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
}
MotionEvent.ACTION_UP -> {
// Pointer is not in contact in the screen.
glFrontBufferRenderer.commit()
}
MotionEvent.CANCEL -> {
// Cancel front buffer; remove last motion set from the screen.
glFrontBufferRenderer.cancel()
}
}
渲染注意事项
在屏幕的一小块区域内进行手写、绘图和素描。
全屏更新、平移、缩放。可能会导致画面撕裂。
画面撕裂
屏幕刷新时,如果同时修改了屏幕缓冲区,就会发生画面撕裂。屏幕的一部分显示新数据,而另一部分则显示旧数据。
动作预测
Jetpack 动作预测库通过估算用户的笔触路径并向渲染程序提供临时的人工点来减少感知延迟时间。
动作预测库以 MotionEvent
对象的形式获取真实的用户输入。这些对象包含有关 x 和 y 坐标、压力和时间的信息,动作预测程序利用这些信息来预测未来的 MotionEvent
对象。
预测的 MotionEvent
对象只是估算值。预测事件可以缩短感知延迟时间,但预测数据在接收后必须替换为实际 MotionEvent
数据。
动作预测库适用于 Android 4.4(API 级别 19)及更高版本,以及搭载 Android 9(API 级别 28)及更高版本的 ChromeOS 设备。
依赖项
运动预测库提供了预测实现。该库会作为依赖项添加到应用的模块 build.gradle
文件中:
dependencies {
implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}
实现
动作预测库包含 MotionEventPredictor
接口,该接口定义了以下方法:
声明一个 MotionEventPredictor
实例
var motionEventPredictor = MotionEventPredictor.newInstance(view)
向预测程序提供数据
motionEventPredictor.record(motionEvent)
预测
when (motionEvent.action) {
MotionEvent.ACTION_MOVE -> {
val predictedMotionEvent = motionEventPredictor?.predict()
if(predictedMotionEvent != null) {
// use predicted MotionEvent to inject a new artificial point
}
}
}
动作预测的注意事项
添加新的预测点后移除旧预测点。
请勿将预测点用于最终渲染。
记事应用
借助 ChromeOS,可以声明应用具有一些记事操作功能。
如需在 ChromeOS 上将应用注册为记事应用,请参阅输入兼容性。
如需在 Android 上将应用注册为记事应用,请参阅创建记事应用。
Android 14(API 级别 34)引入了 ACTION_CREATE_NOTE
intent,可让您的应用在锁定屏幕上启动记事 activity。
利用机器学习套件进行数字手写识别
借助机器学习套件数字手写识别功能,您的应用可以识别数字平面上数百种语言的手写文本。您还可以对素描进行分类。
机器学习套件提供了 Ink.Stroke.Builder
类来创建 Ink
对象,机器学习模型可以对这些对象进行处理,以便将手写内容转换为文本。
除了手写识别之外,该模型还可以识别手势,例如删除和圈起。
如需了解详情,请参阅数字墨水识别。