借助触控笔,用户可以通过新的方式与应用互动:做笔记、素描、完成精细工作,以及通过游戏和娱乐应用进行消遣放松。
Android 和 ChromeOS 提供了多种 API,可在应用中打造出色的触控笔体验。MotionEvent
类提供了有关用户与屏幕互动的信息,包括触控笔压力、方向、倾斜度、悬停距离和手掌检测信息。低延迟图形和动作预测库可以使触控笔在屏幕上的渲染效果更加真实自然,带来堪比使用真纸书写的体验。
MotionEvent
MotionEvent
类表示用户输入互动情况,例如触控指针在屏幕上的位置和移动情况。MotionEvent
还会显示触控笔输入的压力、方向、倾斜度和悬停数据。
事件数据
如需在基于 View 的应用中访问 MotionEvent
数据,请设置一个 onTouchListener:
Kotlin
val onTouchListener = View.OnTouchListener { view, event -> // Process motion event. }
Java
View.OnTouchListener listener = (view, event) -> { // Process motion event. };
监听器会从系统接收 MotionEvent
对象,这样应用就可以处理这些对象。
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
过滤触控笔输入:
Kotlin
val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)
Java
boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);
您还可以使用 TOOL_TYPE_ERASER
指定将触控笔用作橡皮擦:
Kotlin
val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)
Java
boolean 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()

压力
您可以调用以下方法来检索指针压力:
getAxisValue(AXIS_PRESSURE)
或 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
表示指针上移是由于用户无意间轻触所致。该标志通常在用户不小心轻触屏幕时进行设置,例如抓握设备或将手掌放在屏幕上时。
您可以按如下方式访问标志值:
Kotlin
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
Java
boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;
如果已设置此标志,则需要从此指针的最后一次 ACTION_DOWN
操作中撤消最后一组 MotionEvent
集。
与 ACTION_CANCEL
一样,可以通过 getPointerId(actionIndex)
找到指针。
MotionEvent
集。取消手掌轻触操作,并重新渲染展示效果。
全屏、无边框和导航手势
如果某个应用以全屏显示,并且在屏幕边缘附近有可操作的元素(例如绘图或记事应用的画布),则从屏幕底部滑动以显示导航或将应用移至后台,可能会对画布造成不必要的轻触。
为了防止手势在应用中触发不必要的轻触,您可以充分利用边衬区和 ACTION_CANCEL
。
另请参阅上文中的防手掌误触、导航和不必要的输入。
使用 setSystemBarsBehavior()
方法和 WindowInsetsController
的 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
来防止导航手势导致不必要的触摸事件:
Kotlin
// Configure the behavior of the hidden system bars. windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
Java
// Configure the behavior of the hidden system bars. windowInsetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE );
如需详细了解边衬区和手势管理,请参阅:
低延迟时间
延迟时间是指硬件、系统和应用处理和渲染用户输入所需的时间。
延迟时间 = 硬件和操作系统输入处理 + 应用处理 + 系统合成 + 硬件渲染

延迟来源
- 在触摸屏(硬件)上注册触控笔:当触控笔和操作系统通信以注册和同步时,进行初始无线连接。
- 触摸采样率(硬件):触摸屏每秒检查指针是否轻触屏幕表面的次数,范围介于 60 到 1,000Hz 之间。
- 输入处理(应用):对用户输入应用颜色、图形效果和转换功能。
- 图形渲染(操作系统 + 硬件):缓冲区交换、硬件处理。
低延迟图形
借助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
使用回调以最优方式来渲染数据。
Kotlin
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. } }
Java
GLFrontBufferedRenderer.Callback<DATA_TYPE> callbacks = new GLFrontBufferedRenderer.Callback<DATA_TYPE>() { @Override public void onDrawFrontBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, DATA_TYPE data_type) { // OpenGL for front buffer, short, affecting small area of the screen. } @Override public void onDrawDoubleBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, @NonNull Collection<? extends DATA_TYPE> collection) { // OpenGL full scene rendering. } };
声明一个 GLFrontBufferedRenderer 实例
通过提供之前创建的 SurfaceView
和回调来准备 GLFrontBufferedRenderer
。GLFrontBufferedRenderer
会使用回调优化对前端缓冲区和双缓冲区的渲染:
Kotlin
var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Java
GLFrontBufferedRenderer<DATA_TYPE> glFrontBufferRenderer = new GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks);
渲染
当您调用 renderFrontBufferedLayer()
方法(会触发 onDrawFrontBufferedLayer()
回调)时,前端缓冲区渲染就会开始。
当您调用 commit()
功能时,会触发 onDrawMultiDoubleBufferedLayer()
回调。
在以下示例中,当用户开始在屏幕上绘图 (ACTION_DOWN
) 并移动指针 (ACTION_MOVE
) 时,该进程会渲染到前端缓冲区(快速渲染)。当指针离开屏幕表面 (ACTION_UP
) 时,该进程会渲染到双缓冲区。
您可以使用 requestUnbufferedDispatch()
要求输入系统不批量处理动作事件,而是在可用时立即传送事件:
Kotlin
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() } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: { // Deliver input events as soon as they arrive. surfaceView.requestUnbufferedDispatch(motionEvent); // Pointer is in contact with the screen. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_MOVE: { // Pointer is moving. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_UP: { // Pointer is not in contact in the screen. glFrontBufferRenderer.commit(); } break; case MotionEvent.ACTION_CANCEL: { // Cancel front buffer; remove last motion set from the screen. glFrontBufferRenderer.cancel(); } break; }
渲染注意事项
正确做法
在屏幕的一小块区域内进行手写、绘图和素描。
错误做法
全屏更新、平移、缩放。可能会导致画面撕裂。
画面撕裂
当屏幕刷新并同时修改屏幕缓冲区时,会发生画面撕裂。屏幕的一部分显示新数据,而另一部分则显示旧数据。

动作预测
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
实例
Kotlin
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Java
MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
向预测程序提供数据
Kotlin
motionEventPredictor.record(motionEvent)
Java
motionEventPredictor.record(motionEvent);
预测
Kotlin
when (motionEvent.action) { MotionEvent.ACTION_MOVE -> { val predictedMotionEvent = motionEventPredictor?.predict() if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: { MotionEvent predictedMotionEvent = motionEventPredictor.predict(); if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } break; }
动作预测的注意事项
正确做法
添加新的预测点后移除旧预测点。
错误做法
请勿将预测点用于最终渲染。
记事应用
借助 ChromeOS,可以声明应用具有一些记事操作功能。
您可以在应用清单中将应用注册为记事应用。如需了解详情,请参阅输入兼容性。
Android 14(API 级别 34)引入了 ACTION_CREATE_NOTE
intent,可让您的应用在锁定屏幕上启动记事 activity。
利用机器学习套件进行数字手写识别
借助机器学习套件数字手写识别功能,您的应用可以识别数字平面上数百种语言的手写文本。您还可以对素描进行分类。
机器学习套件提供了 Ink.Stroke.Builder
类来创建 Ink
对象,机器学习模型可以对这些对象进行处理,即将手写内容转换为文本形式。
除了手写识别外,该模型还可以识别手势,例如删除和打圈等。
如需了解详情,请参阅数字手写识别。