From 41688ca46ea8a65260ce57eaf112a31a187860f6 Mon Sep 17 00:00:00 2001 From: kingzcheung Date: Sat, 29 Aug 2026 08:51:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(input-method):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E6=A1=86composing=E7=8A=B6=E6=80=81=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在ImeSessionController中添加markInputBoxComposing()调用以标记输入框 composing区域的存在 - 为VoiceRecognitionHandler添加onComposingWritten回调,在语音识别向输入 框写入composing文本时触发标记 - 在XimeInputMethodService中实现inputBoxComposingActive状态变量, 用于跟踪输入框中IME写入的composing区域 - 修改endComposingInputBox方法,仅在存在composing标记时执行 setComposingText("")清理,避免在光标处插入空串导致误删选中文本 - 在输入目标变化时重置composing标记,防止stale标记影响新的输入框 --- .../xime/service/ImeSessionController.kt | 3 ++ .../xime/service/VoiceRecognitionHandler.kt | 5 ++- .../xime/service/XimeInputMethodService.kt | 38 ++++++++++++++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/kingzcheung/xime/service/ImeSessionController.kt b/app/src/main/java/com/kingzcheung/xime/service/ImeSessionController.kt index 413511fb..24546e3b 100644 --- a/app/src/main/java/com/kingzcheung/xime/service/ImeSessionController.kt +++ b/app/src/main/java/com/kingzcheung/xime/service/ImeSessionController.kt @@ -118,6 +118,9 @@ internal class ImeSessionController(private val service: XimeInputMethodService) private fun showInputBoxComposition(ic: android.view.inputmethod.InputConnection, displayText: String) { // 第二参数为 1:光标相对编码起始偏移 1 个字符,使光标落在编码末尾, // 避免传 displayText.length 时被 AOSP 钳制到整段文本末尾(光标跑到最右边)。 + // 标记输入框存在 composing 区域:endComposingInputBox 仅在此标记下执行 setComposingText("") 清空, + // 否则该调用会在光标处插入空串,光标处有选中文字时等于删除选区。 + service.markInputBoxComposing() ic.beginBatchEdit() try { ic.setComposingText(displayText, 1) diff --git a/app/src/main/java/com/kingzcheung/xime/service/VoiceRecognitionHandler.kt b/app/src/main/java/com/kingzcheung/xime/service/VoiceRecognitionHandler.kt index 738366d7..03e0d8e3 100644 --- a/app/src/main/java/com/kingzcheung/xime/service/VoiceRecognitionHandler.kt +++ b/app/src/main/java/com/kingzcheung/xime/service/VoiceRecognitionHandler.kt @@ -20,7 +20,9 @@ class VoiceRecognitionHandler( private val getInputConnection: () -> InputConnection?, private val onVoiceComplete: () -> Unit = {}, private val onAmplitudeChanged: (Float) -> Unit = {}, - private val onSpectrumChanged: (FloatArray) -> Unit = {} + private val onSpectrumChanged: (FloatArray) -> Unit = {}, + /** 语音向输入框写入 composing 文本时回调(标记 composing 区域存在,供 endComposingInputBox 判断)。 */ + private val onComposingWritten: () -> Unit = {} ) { companion object { private const val TAG = "VoiceRecognition" @@ -255,6 +257,7 @@ class VoiceRecognitionHandler( val ic = getInputConnection() if (ic != null) { + onComposingWritten() ic.setComposingText(cleanText, 1) } onStateChanged(getState().copy(voiceRecognizedText = cleanText)) diff --git a/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt b/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt index ab06a54e..85906f42 100644 --- a/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt +++ b/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt @@ -291,7 +291,8 @@ class XimeInputMethodService : InputMethodService(), LifecycleOwner, SavedStateR }, onSpectrumChanged = { spectrum -> voiceSpectrumState.value = spectrum - } + }, + onComposingWritten = { markInputBoxComposing() } ) /** @@ -1540,6 +1541,13 @@ class XimeInputMethodService : InputMethodService(), LifecycleOwner, SavedStateR // 敏感输入框(密码等)判定 + composing 去重标志重置(详见 PluginEventDispatcher) pluginEvents.onStartInput(attribute) + // 输入 target 变化:旧编辑框的 composing 区域不再可达,复位标记。 + // 防御 stale 标记导致 endComposingInputBox 对新编辑框执行 setComposingText("") + // (无 composing 时会在光标处插入空串,选中文字时等于删除选区)。 + if (!restarting) { + inputBoxComposingActive = false + } + predictionManager.clearCommittedText() // 新输入会话清空 partial commit 累积:外部 UI(如设置页输入框"清除"按钮仅清 Compose // state)会触发 restartInput → 此处重建 T9,若残留累积会被 buildT9DisplayState 拼进 @@ -1946,17 +1954,35 @@ class XimeInputMethodService : InputMethodService(), LifecycleOwner, SavedStateR } /** - * 结束输入框中的 composing span,先清空内容再结束,避免转为 committed text。 + * 输入框是否存在 IME 写入的 composing 区域(拼音编码回显 / 语音识别临时文本)。 + * Android 无法查询宿主编辑器的 composing 状态,由写入点主动标记。 + */ + private var inputBoxComposingActive = false + + /** 标记刚向输入框写入了 composing 文本(showInputBoxComposition / 语音 partial)。 */ + internal fun markInputBoxComposing() { + inputBoxComposingActive = true + } + + /** + * 清理输入框中的 composing 区域(未上屏的拼音编码 / 语音临时文本)。 * * 无论输入位置设置(输入框/候选栏)都执行。 - * 英文输入改为直接上屏模式后不再写入 composing 文本(本方法对英文变为空操作), - * 但中文输入框模式仍会产生 composing 区,需在此清理。 + * 注意:composing 区域不存在时 [InputConnection.setComposingText] 并非空操作—— + * 它会在光标处"插入"空串,光标处有选中文字时等于删除整个选区 + * (收起键盘/焦点切换误删选中文字的根因)。因此仅在标记过 composing 时才清空, + * 否则只调用 finishComposingText(无 composing 时是无害空操作,仅兜底清理残留 span)。 */ internal fun endComposingInputBox() { currentInputConnection?.let { - it.setComposingText("", 0) - it.finishComposingText() + if (inputBoxComposingActive) { + it.setComposingText("", 0) + it.finishComposingText() + } else { + it.finishComposingText() + } } + inputBoxComposingActive = false } /**