You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose输入框停止输入后自动格式化富文本实现问题

问题核心原因

现有实现存在三个核心问题,导致无法达到预期效果:

  • 输入状态与防抖后的格式化结果强绑定,用户输入的内容需要等待1秒防抖结束才能同步到输入框,输入过程中会直接丢失字符,输入流程断裂。
  • 格式化后直接生成新的TextFieldValue传入输入框,没有同步维护光标位置,每次重组时光标会强制跳转到文本末尾,用户无法正常在文本中间插入、修改内容。
  • 对同步执行的formatText逻辑多余使用flatMapLatest包装一层流,额外的流转换会带来状态同步的时序差,容易出现文本显示和实际输入不一致的问题。
可行实现方案

整体思路是拆分「实时输入状态」和「防抖格式化结果」两个独立状态:用户输入时立刻同步原始内容到输入框,保证输入流畅不丢字;停止输入1秒后再替换为格式化后的带样式文本,同时手动校正光标位置,避免光标错位。

第一步:调整ViewModel层逻辑

class TextEditViewModel : ViewModel() {
    // 防抖后的格式化输入状态,直接供TextField绑定
    val textFieldState = MutableStateFlow(TextFieldValue(""))
    // 原始输入流,只用来触发防抖计时
    private val rawInputFlow = MutableStateFlow("")

    init {
        rawInputFlow
            .debounce(1000)
            .distinctUntilChanged()
            .map { rawText ->
                // 执行你的富文本解析逻辑,返回带加粗样式的AnnotatedString
                // 注意解析时要记录所有被识别为格式标记、会被删除的*的位置
                val parseResult = parseBoldMarkdown(rawText)
                // 基于格式化前的光标位置,计算格式化后对应的光标偏移
                val newCursorOffset = calculateAdjustedCursor(
                    originalSelection = textFieldState.value.selection,
                    removedCharPositions = parseResult.removedStarPositions,
                    formattedLength = parseResult.annotatedString.length
                )
                TextFieldValue(
                    annotatedString = parseResult.annotatedString,
                    selection = TextRange(newCursorOffset)
                )
            }
            .onEach { formattedValue ->
                textFieldState.value = formattedValue
            }
            .launchIn(viewModelScope)
    }

    // 供Compose层调用的输入回调
    fun onTextInput(newValue: TextFieldValue) {
        // 用户输入瞬间先更新输入框状态,保证输入无延迟、不丢字
        textFieldState.value = newValue
        // 同步纯文本到原始流,重置防抖计时
        rawInputFlow.value = newValue.text
    }

    /**
     * 校正格式化后的光标位置
     * 核心逻辑:原光标位置减去光标前被删除的*的数量,就是格式化后的光标位置
     */
    private fun calculateAdjustedCursor(
        originalSelection: TextRange,
        removedCharPositions: List<Int>,
        formattedLength: Int
    ): Int {
        val originalCursor = originalSelection.start
        val removedBeforeCursor = removedCharPositions.count { it < originalCursor }
        return (originalCursor - removedBeforeCursor).coerceIn(0, formattedLength)
    }
}

/**
 *  markdown加粗解析返回结果
 *  @param annotatedString 解析后带粗体样式的文本
 *  @param removedStarPositions 解析过程中删除的*在原始文本中的位置列表
 */
data class ParseResult(
    val annotatedString: AnnotatedString,
    val removedStarPositions: List<Int>
)

// 这里替换成你自己的formatText实现,记得返回被删除的*的位置列表
private fun parseBoldMarkdown(rawText: String): ParseResult {
    val builder = AnnotatedString.Builder()
    val removedPositions = mutableListOf<Int>()
    var isBold = false
    var index = 0
    while (index < rawText.length) {
        val char = rawText[index]
        if (char == '*') {
            removedPositions.add(index)
            isBold = !isBold
            if (isBold) {
                builder.pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
            } else {
                builder.pop()
            }
        } else {
            builder.append(char)
        }
        index++
    }
    // 处理未闭合的*,避免样式错乱
    if (isBold) builder.pop()
    return ParseResult(builder.toAnnotatedString(), removedPositions)
}

第二步:调整Compose层绑定逻辑

直接绑定ViewModel中维护的输入状态,不要在Compose层自行组装TextFieldValue:

@Composable
fun MarkdownStyleTextField() {
    val viewModel: TextEditViewModel = viewModel()
    val inputValue by viewModel.textFieldState.collectAsState()

    OutlinedTextField(
        value = inputValue,
        onValueChange = { newValue -> viewModel.onTextInput(newValue) },
        label = { Text("Description") },
        modifier = Modifier.fillMaxWidth().fillMaxHeight()
    )
}
注意事项
  • 解析逻辑需要处理未闭合的*、转义\*等边界场景,避免样式错乱。
  • 如果需要更顺滑的体验,可以在用户输入时实时给*设置透明颜色,等防抖结束再替换为粗体样式,避免用户看到*突然消失的跳变感。
  • 如果用户在1秒防抖窗口内继续输入,debounce操作符会自动重置计时,不会出现输入到一半被格式化打断的问题。

内容的提问来源于stack exchange,提问作者San

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 17:12:32