Android 和 ChromeOS 提供了各种 API,可帮助您构建为用户提供出色触笔体验的应用。MotionEvent
类公开了有关触笔与屏幕交互的信息,包括触笔压力、方向、倾斜、悬停和手掌检测。低延迟图形和运动预测库增强了触笔屏幕渲染,以提供自然、类似于笔和纸的体验。
MotionEvent
MotionEvent
类表示用户输入交互,例如屏幕上触摸指针的位置和移动。对于触笔输入,MotionEvent
还公开了压力、方向、倾斜和悬停数据。
事件数据
要访问 MotionEvent
数据,请设置一个 onTouchListener
Kotlin
val onTouchListener = View.OnTouchListener { view, event -> // Process motion event. }
Java
View.OnTouchListener listener = (view, event) -> { // Process motion event. };
侦听器从系统接收 MotionEvent
对象,以便您的应用可以处理它们。
MotionEvent
对象提供与以下 UI 事件方面相关的数据
- 操作:与设备的物理交互——触摸屏幕、在屏幕表面上移动指针、将指针悬停在屏幕表面上
- 指针:与屏幕交互的对象的标识符——手指、触笔、鼠标
- 轴:数据类型——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
操作集称为运动集。
指针
大多数屏幕都是多点触控:系统为每个手指、触笔、鼠标或其他与屏幕交互的指向对象分配一个指针。指针索引使您能够获取特定指针的轴信息,例如触摸屏幕的第一个手指或第二个手指的位置。
指针索引范围从零到 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()
压力
您可以使用 MotionEvent#getAxisValue(AXIS_PRESSURE)
或第一个指针的 MotionEvent#getPressure()
检索指针压力。
触摸屏或触摸板的压力值介于 0(无压力)和 1 之间,但根据屏幕校准,可能会返回更高的值。
方向
方向指示触笔指向的方向。
可以使用 getAxisValue(AXIS_ORIENTATION)
或 getOrientation()
(对于第一个指针)检索指针方向。
对于触笔,方向以弧度值返回,介于 0 到 pi(𝛑)顺时针或 0 到 -pi 逆时针之间。
方向使您能够实现真实的画笔。例如,如果触笔代表扁平画笔,则扁平画笔的宽度取决于触笔方向。
倾斜
倾斜测量触笔相对于屏幕的倾斜度。
倾斜以弧度返回触笔的正角,其中零垂直于屏幕,𝛑/2 平放在表面上。
可以使用 getAxisValue(AXIS_TILT)
(第一个指针没有快捷方式)检索倾斜角。
倾斜可用于尽可能再现真实的工具,例如模拟用倾斜的铅笔进行阴影。
悬停
可以通过 getAxisValue(AXIS_DISTANCE)
获取触控笔与屏幕的距离。该方法返回一个从 0.0(接触屏幕)到较高值(触控笔远离屏幕)的值。屏幕和触控笔笔尖(点)之间的悬停距离取决于屏幕和触控笔的制造商。由于实现可能有所不同,因此不要依赖于精确的值来实现应用关键功能。
触控笔悬停可用于预览画笔大小或指示将要选择按钮。
注意: Compose 提供了影响 UI 元素交互状态的修饰符
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;
如果设置了标志,则需要撤消最后一个 MotionEvent
集,从该指针的最后一个 ACTION_DOWN
开始。
与 ACTION_CANCEL
类似,可以使用 getPointerId(actionIndex)
找到指针。
全屏、边缘到边缘和导航手势
如果应用是全屏的并且在边缘附近有可操作的元素,例如绘图或笔记应用的画布,则从屏幕底部向上滑动以显示导航或将应用移到后台可能会导致画布上出现不需要的触摸。
为了防止手势在您的应用中触发不需要的触摸,您可以利用 insets 和 ACTION_CANCEL
。
另请参阅 手掌拒绝、导航和不需要的输入 部分。
使用 setSystemBarsBehavior()
方法和 BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
of WindowInsetsController
防止导航手势导致不需要的触摸事件
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 到 1000Hz。
- 输入处理(应用):对用户输入应用颜色、图形效果和变换。
- 图形渲染(操作系统 + 硬件):缓冲区交换、硬件处理。
低延迟图形
Jetpack 低延迟图形库 减少了用户输入和屏幕渲染之间的处理时间。
该库通过避免多缓冲渲染并利用前缓冲渲染技术来减少处理时间,这意味着直接写入屏幕。
前缓冲渲染
前缓冲区是屏幕用于渲染的内存。它是应用最接近直接绘制到屏幕的地方。低延迟库使应用能够直接渲染到前缓冲区。通过防止缓冲区交换来提高性能,缓冲区交换可能发生在常规多缓冲渲染或双缓冲渲染(最常见的情况)中。
虽然前缓冲渲染是渲染屏幕小区域的绝佳技术,但它并非旨在用于刷新整个屏幕。使用前缓冲渲染时,应用将内容渲染到显示器从中读取的缓冲区中。因此,有可能出现渲染伪像或 撕裂(请参见下文)。
低延迟库适用于 Android 10(API 级别 29)及更高版本以及运行 Android 10(API 级别 29)及更高版本的 Chrome OS 设备。
依赖项
低延迟库提供了前缓冲渲染实现的组件。该库作为应用模块 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)及更高版本的 Chrome OS 设备。
依赖项
运动预测库提供了预测的实现。该库作为应用模块 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 允许您的应用声明一些笔记功能。
要将应用注册为 ChromeOS 上的笔记应用,请参阅 输入兼容性。
要将应用注册为 Android 上的笔记应用,请参阅 创建笔记应用。
Android 14(API 级别 34)引入了 ACTION_CREATE_NOTE
意图,使您的应用能够在锁屏上启动笔记活动。
使用 ML Kit 进行数字墨水识别
借助 ML Kit 数字墨水识别,您的应用可以在数百种语言中识别数字表面上的手写文本。您还可以对草图进行分类。
ML Kit 提供了 Ink.Stroke.Builder
类来创建 Ink
对象,这些对象可以由机器学习模型处理,以将手写转换为文本。
除了手写识别之外,模型还可以识别 手势,例如删除和圈选。
请参阅 数字墨水识别,以了解更多信息。