自定义文本编辑器

自定义文本编辑器是既不是 EditText 组件也不是 WebView 文本小部件,但通过实现 onCreateInputConnection() 回调来支持文本输入,该回调在视图获得焦点且系统请求 InputConnection 时调用视图。

来自自定义文本编辑器的对 onCheckIsTextEditor() 的调用应该返回 true

在自定义文本编辑器中支持手写笔手写

Android 14(API 级别 34)及更高版本默认情况下支持标准 Android 文本输入组件中的手写笔输入(请参阅 文本字段中的手写笔输入)。但是,自定义文本输入字段(或编辑器)需要额外的开发。

要创建自定义文本编辑器,请执行以下操作

  1. 启用手写启动
  2. 声明手写支持
  3. 支持手写手势(选择、删除、插入等)
  4. 向 IME 提供光标位置和其他位置数据
  5. 显示手写笔手写悬停图标

启用手写启动

如果视图仅由单个文本编辑器组成,则视图系统可以自动为视图启动手写笔手写。否则,视图必须实现自己的手写启动逻辑。

自动手写启动

如果视图显示单个文本编辑器且没有其他内容,则视图可以通过调用 setAutoHandwritingEnabled(true) 选择加入视图系统的自动手写启动。

启用自动手写后,从视图的手写边界内的任何位置开始的手写笔移动都会自动启动手写模式。输入法编辑器 (IME) 会接收手写笔运动事件并提交识别出的文本。

Input field with surrounding rectangle indicating the bounds for detection of stylus motion events.
图 1.EditText 字段的边界内手写。

自定义手写启动

如果视图除了单个文本编辑器之外还包含多个文本编辑器或内容,则视图必须按照以下步骤实现自己的手写启动逻辑

  1. 通过调用 setAutoHandwritingEnabled(false) 选择退出视图系统的自动手写启动。

  2. 跟踪视图中可见的所有文本编辑器。

  3. dispatchTouchEvent() 中监视视图收到的运动事件。

如果文本编辑器位于可滚动视图中,则应将编辑器手写边界内的手写笔移动视为手写,而不是滚动。使用 ViewParent#requestDisallowInterceptTouchEvent() 阻止可滚动祖先视图拦截来自文本编辑器的触摸事件。

API 详细信息

  • MotionEvent#getToolType() — 指示 MotionEvent 是否来自手写笔,如果是,则返回值为 TOOL_TYPE_STYLUSTOOL_TYPE_ERASER

  • InputMethodManager#isStylusHandwritingAvailable() — 指示 IME 是否支持手写笔手写。在每次调用 InputMethodManager#startStylusHandwriting() 之前调用此方法,因为手写可用性可能已更改。

  • InputMethodManager#startStylusHandwriting() — 使 IME 进入手写模式。一个 ACTION_CANCEL 运动事件被分派到应用以取消当前手势。手写笔运动事件不再分派到应用。

    已分派到应用的当前手势的手写笔运动事件将转发到 IME。IME 需要显示一个手写笔墨水窗口,IME 通过该窗口接收所有后续的 MotionEvent 对象。IME 使用 InputConnection API 提交识别出的手写文本。

    如果 IME 无法进入手写模式,则此方法调用将成为无操作。

声明手写支持

在填充 EditorInfo 参数时,View#onCreateInputConnection(EditorInfo) 调用 setStylusHandwritingEnabled() 以告知 IME 文本编辑器支持手写。使用 setSupportedHandwritingGestures()setSupportedHandwritingGesturePreviews() 声明支持的手势。

支持手写手势

IME 可以支持各种手写手势,例如圈出文本以选择它或在文本上乱涂以删除它。

图 2. 圈出以选择文本。
图 3. 乱涂以删除文本。

自定义编辑器实现 InputConnection#performHandwritingGesture()InputConnection#previewHandwritingGesture() 以支持不同的 HandwritingGesture 类型,例如 SelectGestureDeleteGestureInsertGesture

在填充 EditorInfo 参数时,声明支持的手写手势,View#onCreateInputConnection(EditorInfo)(请参阅 声明手写支持 部分)。

API 详细信息

  • InputConnection#performHandwritingGesture(HandwritingGesture, Executor, IntConsumer) — 实现手势。 HandwritingGesture 参数包含位置信息,您可以使用这些信息来确定在文本中的哪个位置执行手势。例如,SelectGesture 提供一个 RectF 对象,该对象指定所选文本范围,而 InsertGesture 提供一个 PointF 对象,该对象指定要插入文本的文本偏移量。

    使用 ExecutorIntConsumer 参数来发送操作结果。当同时提供执行程序和使用者参数时,使用执行程序调用 IntConsumer#accept(),例如

    
    executor.execute { consumer.accept(HANDWRITING_GESTURE_RESULT_SUCCESS) }
    
    
  • HandwritingGesture#getFallbackText() — 提供回退文本,如果手写手势区域下方没有适用文本,则 IME 会在光标位置提交此文本。

    有时,IME 无法确定手写笔手势是打算执行手势操作还是手写文本。自定义文本编辑器负责确定用户的意图并在手势位置执行适当的操作(取决于上下文)。

    例如,如果 IME 无法确定用户是要绘制向下箭头 ⋁ 以执行插入空格手势,还是要手写字母“v”,则 IME 可以发送一个带有回退文本“v”的 InsertGesture

    编辑器应首先尝试执行插入空格手势。如果手势无法执行(例如,指定位置没有文本),编辑器应退回到在光标位置插入“v”。

  • InputConnection#previewHandwritingGesture(PreviewableHandwritingGesture, CancellationSignal) — 预览正在进行的手势。例如,当用户开始在一些文本周围画一个圆圈时,可以显示结果选择的实时预览,并在用户继续绘制时不断更新。只有某些手势类型可以预览(参见 PreviewableHandwritingGesture)。

    IME 可以使用 CancellationSignal 参数来取消预览。如果其他事件中断了预览(例如,文本以编程方式更改或发生新的 InputConnection 命令),自定义编辑器可以取消预览。

    预览手势仅用于显示,不应该改变编辑器状态。例如,SelectGesture 预览隐藏了编辑器的当前选择范围,并突出显示了手势预览范围。但一旦取消预览,编辑器应恢复其之前的选择范围。

提供光标位置和其他位置数据

在手写模式下,IME 可以使用 InputConnection#requestCursorUpdates() 请求光标位置和其他位置数据。自定义编辑器通过调用 InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo) 来响应。与手写相关的 CursorAnchorInfo 中的数据通过以下 CursorAnchorInfo.Builder 方法提供

  • setInsertionMarkerLocation() — 设置光标位置。IME 使用该值将手写墨迹动画到光标位置。
  • setEditorBoundsInfo() — 设置编辑器的边界和手写边界。IME 使用此数据将 IME 的手写工具栏定位在屏幕上。
  • addVisibleLineBounds() — 设置编辑器所有可见(或部分可见)文本行的边界。IME 使用行边界来提高识别手势的准确性。
  • setTextAppearanceInfo() — 设置文本外观,包含从文本输入字段获取的信息。IME 使用此信息来为手写墨迹设置样式。

显示手写笔手写悬停图标

当触控笔悬停在您自定义文本编辑器的手写边界上,并且所选 IME 支持触控笔手写时,显示触控笔手写悬停图标 (InputMethodManager#isStylusHandwritingAvailable())。

覆盖 View#onResolvePointerIcon() 以获取触控笔手写的悬停图标。在覆盖中,调用 PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING) 来访问系统触控笔手写悬停图标。

其他资源