Compose SelectionState API — 主动控制文本选择

各位 Compose 爱好者们,早上好,今天我们来看一个 Compose 的新特性,SelectionState API ------ 可以主动选择文本的能力。

我在之前的一篇文章破坏性更新 - 解读 Jetpack Compose 1.12中,提到过这个变更。

以往的 Compose 中,实际上已经有一个可以实现选择的控件 ------ SelectionContainer,但它更像是一个纯粹的交互容器:把文本放进去,用户就可以长按、拖动来选择内容。

如果你想在代码中主动做点事情------例如点击按钮后全选、清除当前选择,或者把已选文本同步到界面其他位置------可用的入口并不多。

Compose 1.12.0 新增的 SelectionState,就是用来解决这个问题的。

有了它,选择状态可以被提升出来,供外部观察、双向控制,而不再只是容器内部的临时交互状态。

SelectionState 是什么

SelectionState 用来保存某个 SelectionContainer 的选择状态。它既能让我们拿到当前选中的文本,也提供了一组方法,用于在代码中修改选择范围。

在 Composable 中,通常通过 rememberSelectionState() 创建:

kotlin 复制代码
val selectionState = rememberSelectionState()

然后把它交给对应的 SelectionContainer

kotlin 复制代码
SelectionContainer(state = selectionState) {
    Text(
        "可以通过代码主动选择的文本内容。"
    )
}

这里有一个需要注意的约束:

同一个 SelectionState 不能同时传给多个 SelectionContainer

它描述的是某一个容器的选择状态,而不是全局的选择管理器。多个容器需要独立选择时,就应分别创建各自的 SelectionState

另外,rememberSelectionState() 会自动处理状态保存与恢复;如果是更复杂的状态管理场景,也可以配合 SelectionState.SaverrememberSaveable 使用。

主动控制选择范围

SelectionState 提供了四个直接的控制方法。

全选

selectAll() 会选择容器中全部可选择的 Text 内容。

kotlin 复制代码
Button(onClick = { selectionState.selectAll() }) {
    Text("全选")
}

需要留意的是,如果把 SelectionContainer 放在 LazyColumn 这类懒布局的外层,selectAll() 只能选择当前已经进入 Composition 的文本,并不会自动把未组合的列表项也选中。

清除选择

clear() 会清除当前选择,同时移除选择手柄。

kotlin 复制代码
Button(onClick = { selectionState.clear() }) {
    Text("清除选择")
}

指定范围选择

select(TextRange) 可以选中指定范围的文本:

kotlin 复制代码
Button(onClick = { selectionState.select(TextRange(0, 4)) }) {
    Text("选择指定范围")
}

这里的 TextRange 是相对于整个 SelectionContainer 内全部 Text全局文本位置 ,而不是某一个单独 Text 的局部下标。

这意味着,即使容器内有多个 Text,也可以用一个范围跨越它们进行选择。

按单词扩展

extendSelectionByWord() 会把当前选择扩展到下一个单词边界。

kotlin 复制代码
Button(onClick = { selectionState.extendSelectionByWord() }) {
    Text("按单词扩展")
}

它的行为有两个细节:

  • 如果当前没有选择内容,会先选中容器中的第一个单词;
  • 如果下一个单词位于另一个 Text 中,选择会继续扩展到下一个 Text

这里的"单词边界"遵循 Unicode 的文本分词规则,因此空格、标点等非单词字符也会参与边界判断。若用户从右向左反向选择,扩展方向也会跟随当前选择的活动端,而不是始终向后增加内容。

读取当前选中的文本

控制选择只是第一步,更实用的是我们终于可以直接观察"用户到底选了什么"。

selectionState.selectedTexts 会返回当前被选中的文本:

kotlin 复制代码
val selectedTexts: List<AnnotatedString> = selectionState.selectedTexts

返回值是 List<AnnotatedString>,并且按照界面中的视觉/布局顺序排列。容器中每个参与选择的 Text,都会作为一个 AnnotatedString 出现在这个列表里。

这个属性由 mutableStateOf 支撑,因此它本身就是可观察的 Compose State。选择范围变化后,读取它的 Composable 会自动重组:

kotlin 复制代码
Text(text = "当前选中:${selectionState.selectedTexts}")

实际项目里,一般会把它转换成你真正需要的形式,例如用于展示已选字符数、复制内容预览,或者驱动一组只在存在选择时才显示的操作按钮。

完整示例

下面这个示例把前面的几个能力放在一起:全选、清除、按范围选择、按单词扩展,以及把当前选择结果直接展示在界面上。

kotlin 复制代码
@Composable
fun ProgrammaticSelectionExample() {
    val selectionState = rememberSelectionState()

    Column {
        Row {
            Button(
                onClick = { selectionState.selectAll() },
            ) {
                Text("全选")
            }

            Button(
                onClick = { selectionState.clear() },
            ) {
                Text("清除选择")
            }
        }

        Row {
            Button(
                onClick = { selectionState.select(TextRange(0, 4)) },
            ) {
                Text("选择指定范围")
            }
            Button(
                onClick = { selectionState.extendSelectionByWord() },
            ) {
                Text("按单词扩展")
            }
        }

        SelectionContainer(state = selectionState) {
            Text(
                "可以通过代码主动选择的文本内容。",
                style = MaterialTheme.typography.headlineLarge
            )
        }

        Text(text = "当前选中:${selectionState.selectedTexts}")
    }
}

原文中的演示效果如下:

总结

SelectionState 本身不是大的 API 变更,但它让 SelectionContainer 的选择除了靠用户手势,也能由代码主动驱动。

当你的页面需要实现选中文本的计数、复制前预览、批量操作,或者给文本选择提供额外快捷操作时,就不必再绕过 Compose 的选择机制,自己维护一套选择范围了。

它的使用方式也很符合 Compose 一贯的思路:把状态创建在合适的位置,上提给需要读取和操作它的组件;容器负责渲染与手势,外层则根据状态作出响应。

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work1 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work1 天前
ch21 签名、校验与发版
kotlin
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui