高级触控笔功能

试试 Compose 方法
Jetpack Compose 是 Android 推荐的界面工具包。了解如何在 Compose 中使用触控笔。

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 对象提供与界面事件以下方面相关的数据

  • 操作:与设备的物理交互 - 触摸屏幕、在屏幕表面上移动指针、在屏幕表面上悬停指针
  • 指针:与屏幕交互的对象的标识符 - 手指、触控笔、鼠标
  • 轴:数据类型 - 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_DOWNACTION_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_DOWNACTION_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 坐标:

Stylus drawing on screen with x and y coordinates mapped.
图 1. 触控笔指针的屏幕 x 和 y 坐标。

压力

您可以使用 MotionEvent#getAxisValue(AXIS_PRESSURE) 或对于第一个指针为 MotionEvent#getPressure() 检索指针压力。

对于触摸屏或触摸板,压力值为 0(无压力)到 1 之间,但根据屏幕校准可能会返回更高值。

Stylus stroke that represents a continuum of low to high pressure. The stroke is narrow and faint on the left, indicating low pressure. The stroke becomes wider and darker from left to right until it is widest and darkest on the far right, indicating highest pressure.
图 2. 压力表示 - 左侧低压力,右侧高压力。

方向

方向指示触控笔指向哪个方向。

可以使用 getAxisValue(AXIS_ORIENTATION)getOrientation()(对于第一个指针)检索指针方向。

对于触控笔,方向以弧度值返回,范围为 0 到 pi (𝛑) 顺时针或 0 到 -pi 逆时针。

方向使您能够实现真实的画笔。例如,如果触控笔代表扁平画笔,则扁平画笔的宽度取决于触控笔的方向。

图 3. 触控笔向左指向大约 -0.57 弧度。

倾斜度

倾斜度测量触控笔相对于屏幕的倾斜度。

倾斜度以弧度返回触控笔的正角,其中零垂直于屏幕,𝛑/2 平放在表面上。

倾斜角可以使用 getAxisValue(AXIS_TILT)(第一个指针没有快捷方式)检索。

倾斜度可用于尽可能接近地复制真实工具,例如模仿倾斜铅笔进行阴影处理。

Stylus inclined about 40 degrees from the screen surface.
图 4. 触控笔倾斜约 0.785 弧度,即偏离垂直方向 45 度。

悬停

触控笔与屏幕的距离可以通过 getAxisValue(AXIS_DISTANCE) 获取。该方法返回一个从 0.0(与屏幕接触)到触控笔远离屏幕时增加的值。屏幕和触控笔笔尖(点)之间的悬停距离取决于屏幕和触控笔的制造商。由于实现方式可能不同,因此不要依赖精确值来实现应用的关键功能。

触控笔悬停可用于预览画笔大小或指示即将选择一个按钮。

图 5. 触控笔悬停在屏幕上方。即使触控笔未触及屏幕表面,应用也会做出反应。

注意: Compose 提供影响界面元素交互状态的修饰符:

  • hoverable:使用指针进入和退出事件配置组件以可悬停。
  • indication:在交互发生时为此组件绘制视觉效果。

手掌误触、导航和意外输入

有时,多点触控屏幕会注册到意外触摸,例如用户在手写时自然地将手放在屏幕上以获得支撑。手掌误触是一种检测此行为并通知您应取消最后一次 MotionEvent 集的机制。

因此,您必须保留用户输入的历史记录,以便可以从屏幕上移除意外触摸,并重新渲染合法的用户输入。

ACTION_CANCEL 和 FLAG_CANCELED

ACTION_CANCELFLAG_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) 找到指针。

图 6. 触控笔笔触和手掌触碰创建 MotionEvent 集。手掌触碰被取消,显示屏重新渲染。

全屏、全边以及导航手势

如果某个应用是全屏的,并且其边缘附近有可操作的元素(例如绘图或记事应用的画布),则从屏幕底部滑动以显示导航或将应用移至后台可能会导致意外触碰到画布。

图 7. 滑动手势可将应用移至后台。

为了防止手势在您的应用中触发意外触摸,您可以利用 InsetsACTION_CANCEL

另请参阅手掌误触、导航和意外输入部分。

使用 setSystemBarsBehavior() 方法和 WindowInsetsControllerBEHAVIOR_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
);

如需详细了解插页和手势管理,请参阅:

低延迟

延迟是硬件、系统和应用处理和渲染用户输入所需的时间。

延迟 = 硬件和操作系统输入处理 + 应用处理 + 系统合成

  • 硬件渲染
Latency causes the rendered stroke to lag behind the stylus position. The gap between the stroke rendered and the stylus position represents the latency.
图 8. 延迟导致渲染的笔触落后于触控笔位置。

延迟来源

  • 触控笔与触摸屏注册(硬件):触控笔和操作系统通信以进行注册和同步的初始无线连接。
  • 触摸采样率(硬件):触摸屏每秒检查指针是否触摸表面的次数,范围从 60 到 1000Hz。
  • 输入处理(应用):对用户输入应用颜色、图形效果和变换。
  • 图形渲染(操作系统 + 硬件):缓冲区交换、硬件处理。

低延迟图形

Jetpack 低延迟图形库减少了用户输入和屏幕渲染之间的处理时间。

该库通过避免多缓冲区渲染并利用前缓冲区渲染技术(即直接写入屏幕)来减少处理时间。

前缓冲区渲染

前缓冲区是屏幕用于渲染的内存。它是应用最接近直接在屏幕上绘制的地方。低延迟库使应用可以直接渲染到前缓冲区。这通过阻止缓冲区交换来提高性能,缓冲区交换可能发生在常规多缓冲区渲染或双缓冲区渲染(最常见的情况)中。

App writes to screen buffer and reads from screen buffer.
图 9. 前缓冲区渲染。
App writes to multi-buffer, which swaps with screen buffer. App reads from screen buffer.
图 10. 多缓冲区渲染。

虽然前缓冲区渲染是一种用于渲染屏幕小区域的优秀技术,但它并非设计用于刷新整个屏幕。使用前缓冲区渲染时,应用会将内容渲染到显示屏正在读取的缓冲区中。因此,可能会出现渲染伪影或撕裂(见下文)。

低延迟库可用于 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 和回调来准备 GLFrontBufferedRendererGLFrontBufferedRenderer 使用您的回调优化到前缓冲区和双缓冲区的渲染。

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;
}

渲染注意事项

✓ 应

屏幕小区域、手写、绘图、草图。

✗ 不应

全屏更新、平移、缩放。可能会导致撕裂。

撕裂

撕裂发生在屏幕刷新时,屏幕缓冲区同时被修改。屏幕的一部分显示新数据,而另一部分显示旧数据。

Upper and lower parts of Android image are misaligned due to tearing as screen refreshes.
图 11. 屏幕从上到下刷新时发生撕裂。

运动预测

Jetpack 运动预测库通过估算用户的笔触路径并向渲染器提供临时的、人工的点来减少感知延迟。

运动预测库将真实用户输入作为 MotionEvent 对象获取。这些对象包含有关 x 和 y 坐标、压力和时间的信息,运动预测器利用这些信息来预测未来的 MotionEvent 对象。

预测的 MotionEvent 对象只是估计值。预测的事件可以减少感知延迟,但一旦接收到实际的 MotionEvent 数据,必须将其替换为预测的数据。

运动预测库可用于 Android 4.4(API 级别 19)及更高版本以及运行 Android 9(API 级别 28)及更高版本的 ChromeOS 设备。

Latency causes the rendered stroke to lag behind the stylus position. The gap between the stroke and stylus is filled with prediction points. The remaining gap is the perceived latency.
图 12. 通过运动预测减少延迟。

依赖项

运动预测库提供预测的实现。该库作为依赖项添加到应用的模块 build.gradle 文件中

dependencies {
    implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}

实现

运动预测库包含 MotionEventPredictor 接口,该接口定义了以下方法:

  • record():存储 MotionEvent 对象作为用户操作的记录。
  • predict():返回预测的 MotionEvent
声明 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 Intent,该 Intent 使您的应用能够在锁定屏幕上启动记事 Activity。

使用 ML Kit 进行数字墨水识别

借助 ML Kit 数字墨水识别,您的应用可以在数字表面上识别数百种语言的手写文本。您还可以对草图进行分类。

ML Kit 提供了 Ink.Stroke.Builder 类,用于创建可由机器学习模型处理以将手写字迹转换为文本的 Ink 对象。

除了手写识别,该模型还能识别手势,例如删除和圈选。

请参阅数字墨水识别以了解详情。

其他资源

开发者指南

Codelabs