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
对象提供与 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
过滤触控笔输入
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 逆时针的弧度值返回。
方向使您能够实现真实的画笔。例如,如果触控笔表示扁平画笔,则扁平画笔的宽度取决于触控笔的方向。
倾斜
倾斜测量触控笔相对于屏幕的倾斜度。
倾斜以弧度返回触控笔的正角度,其中零垂直于屏幕,𝛑/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
指示向上移动的指针是无意的用户触摸。当用户意外触摸屏幕时,例如握住设备或将手掌放在屏幕上时,通常会设置此标志。
您可以按如下方式访问标志值
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
如果设置了标志,则需要撤消上一个MotionEvent
集,从该指针的最后一个ACTION_DOWN
开始。
与ACTION_CANCEL
类似,可以使用getPointerId(actionIndex)
找到指针。
全屏、边缘到边缘和导航手势
如果应用程序是全屏的并且在边缘附近有可操作的元素,例如绘图或笔记应用程序的画布,则从屏幕底部向上滑动以显示导航或将应用程序移到后台可能会导致画布上出现不需要的触摸。
为了防止手势在您的应用程序中触发不需要的触摸,您可以利用insets 和 ACTION_CANCEL
。
另请参阅手掌拒绝、导航和不需要的输入部分。
使用setSystemBarsBehavior()
方法和BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
的WindowInsetsController
来防止导航手势导致不需要的触摸事件
// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
要了解有关插入和手势管理的更多信息,请参阅
低延迟
延迟是硬件、系统和应用程序处理和渲染用户输入所需的时间。
延迟 = 硬件和操作系统输入处理 + 应用程序处理 + 系统合成
- 硬件渲染
延迟来源
- 使用触摸屏注册手写笔(硬件):手写笔和操作系统进行通信以进行注册和同步时的初始无线连接。
- 触摸采样率(硬件):触摸屏每秒检查指针是否接触表面的次数,范围从 60 到 1000Hz。
- 输入处理(应用程序):对用户输入应用颜色、图形效果和变换。
- 图形渲染(操作系统 + 硬件):缓冲区交换、硬件处理。
低延迟图形
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)
渲染
当您调用触发onDrawFrontBufferedLayer()
回调的renderFrontBufferedLayer()
方法时,前缓冲渲染开始。
当您调用触发onDrawMultiDoubleBufferedLayer()
回调的commit()
函数时,双缓冲渲染恢复。
在下面的示例中,当用户开始在屏幕上绘制(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
数据,就必须用实际的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
意图,使您的应用程序能够在锁定屏幕上启动笔记活动。
使用 ML Kit 进行数字墨水识别
使用ML Kit 数字墨水识别,您的应用程序可以识别数百种语言的数字表面上的手写文本。您还可以对草图进行分类。
ML Kit 提供了Ink.Stroke.Builder
类来创建可由机器学习模型处理的Ink
对象,以将手写转换为文本。
除了手写识别之外,该模型还能够识别手势,例如删除和圆圈。
请参阅数字墨水识别以了解更多信息。