查看: 134|回复: 1

TextController动态文本选择高亮实战:HarmonyOS NEXT 6.1新特性解析

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在鸿蒙应用开发中,文本交互的深度与流畅度直接影响用户体验。以往,普通Text组件的文本选区依赖于用户长按或双击手势,开发者无法通过代码主动控制高亮区域,这限制了AI划词、搜索高亮、语音同步标注等创新功能的实现。HarmonyOS NEXT 6.1(API 23)对ArkUI的TextController进行了里程碑式升级,新增了setTextSelection()接口,使开发者能够通过代码动态指定文本索引区间,触发原生高亮框选,并支持系统菜单联动。本文将结合实战,深入解析该接口的能力、使用方法和常见避坑点。

一、能力与原理
在ArkUI的文本渲染管道中,Text组件开启复制能力(如设置copyOption)后,底层会激活选区高亮渲染层。API 23之前,该层完全由系统手势状态机控制,应用层无法介入。setTextSelection()的出现打破了这一封闭链路:调用时,指令绕过手势层,直接将起始索引(selectionStart)和结束索引(selectionEnd)注入底层状态机,文本测量层(Paragraph)立即计算字符物理坐标,在无需重排的前提下局部叠加蓝色高亮色块,并可根据选项决定是否弹出系统级复制、全选菜单。

二、API定义与约束
TextController上的setTextSelection()方法原型如下:
  1. setTextSelection(
  2.   selectionStart: number | undefined,
  3.   selectionEnd: number | undefined,
  4.   options?: SelectionOptions
  5. ): void;
复制代码
参数说明:
- selectionStart:起始索引,范围[0, +∞)。负数或undefined强制按0处理。
- selectionEnd:结束索引,范围[0, +∞)。负数或undefined强制按0处理。
- options:可选,SelectionOptions对象,其menuPolicy字段支持SHOW(弹出菜单)、HIDE(隐藏菜单)、DEFAULT(默认行为)。

生效前置条件:
- 必须将Text组件的copyOption设置为非None值(如CopyOptions.InApp或CopyOptions.LocalDevice),否则调用静默失效。
- 不能与跑马灯模式(TextOverflow.MARQUEE)同时使用,否则不生效。

边界规则:
- 当selectionStart ≥ selectionEnd时,视为无效选区,无任何选中效果。
- 索引超出文本总字符数(textSize)时,自动截断为textSize边界。
- 若选中索引对应的字符因截断(如textOverflow)而不可见,则高亮不生效。
- 在PC/2in1设备上,即使设置MenuPolicy.SHOW也不会主动弹出菜单,以符合桌面交互习惯。
- Emoji字符会作为整体被完整选中。

三、实战代码落地
下面是一个完整的Demo示例,展示了如何在ArkUI中使用TextController实现动态选区高亮,并提供索引调整面板和诊断日志。
  1. import { router } from '@kit.ArkUI';
  2. @Entry
  3. @Component
  4. struct TextSelectionDetail {
  5.   private textController: TextController = new TextController();
  6.   @State selectionStartInput: number = 0;
  7.   @State selectionEndInput: number = 10;
  8.   @State selectionLogs: string = '输入起始和结束索引,点击按钮触发动态选择高亮';
  9.   private testTextValue: string = 'HarmonyOS NEXT 极客排版与动态选区高亮技术(API 23)';
  10.   build() {
  11.     Column() {
  12.       // 导航栏
  13.       Row() {
  14.         Button('返回')
  15.           .onClick(() => { router.back(); })
  16.           .backgroundColor('#F5A623').fontColor('#000000').height(36).margin({ right: 16 })
  17.         Text('TextController 动态选区高亮实战')
  18.           .fontColor('#FFFFFF').fontSize(18).fontWeight(FontWeight.Bold)
  19.       }
  20.       .width('100%').height(56).backgroundColor('#111111').padding({ left: 16, right: 16 })
  21.       Column() {
  22.         // 文本展示区
  23.         Column() {
  24.           Text('【动态选区测试 Text 组件】')
  25.             .fontSize(14).fontColor('#F5A623').fontWeight(FontWeight.Bold).margin({ bottom: 12 })
  26.           Text(this.testTextValue, { controller: this.textController })
  27.             .fontSize(16).fontColor('#FFFFFF').lineHeight(24)
  28.             .copyOption(CopyOptions.InApp) // 核心:必须设置非None
  29.             .width('100%').padding(12).backgroundColor('#111111').borderRadius(8).margin({ bottom: 8 })
  30.           Text(`文本总字符数 (textSize):${this.testTextValue.length}`)
  31.             .fontSize(12).fontColor('#888888')
  32.         }
  33.         .width('100%').padding(16).backgroundColor('#161616').borderRadius(10)
  34.         .border({ width: 1, color: '#2C2C2C' }).margin({ bottom: 20 })
  35.         // 索引控制面板
  36.         Column() {
  37.           Text('【选区索引精确控制器】')
  38.             .fontSize(14).fontColor('#F5A623').fontWeight(FontWeight.Bold).margin({ bottom: 12 })
  39.           Row({ space: 12 }) {
  40.             Column() {
  41.               Text('起始位置 (Start)').fontSize(12).fontColor('#A0A0A0').margin({ bottom: 6 })
  42.               TextInput({ text: this.selectionStartInput.toString() })
  43.                 .width('100%').height(40).type(InputType.Number)
  44.                 .backgroundColor('#1A1A1A').fontColor('#FFFFFF')
  45.                 .onChange((value: string) => { this.selectionStartInput = parseInt(value) || 0; })
  46.             }.flexGrow(1)
  47.             Column() {
  48.               Text('结束位置 (End)').fontSize(12).fontColor('#A0A0A0').margin({ bottom: 6 })
  49.               TextInput({ text: this.selectionEndInput.toString() })
  50.                 .width('100%').height(40).type(InputType.Number)
  51.                 .backgroundColor('#1A1A1A').fontColor('#FFFFFF')
  52.                 .onChange((value: string) => { this.selectionEndInput = parseInt(value) || 0; })
  53.             }.flexGrow(1)
  54.           }
  55.           .width('100%').margin({ bottom: 16 })
  56.           Row({ space: 10 }) {
  57.             Button('触发 setTextSelection')
  58.               .onClick(() => {
  59.                 try {
  60.                   this.textController.setTextSelection(
  61.                     this.selectionStartInput,
  62.                     this.selectionEndInput,
  63.                     { menuPolicy: MenuPolicy.SHOW }
  64.                   );
  65.                   this.selectionLogs = `执行选区高亮:[${this.selectionStartInput}, ${this.selectionEndInput}] 成功触发`;
  66.                 } catch (err) {
  67.                   this.selectionLogs = `执行失败:${JSON.stringify(err)}`;
  68.                 }
  69.               })
  70.               .backgroundColor('#E65100').fontColor('#FFFFFF').height(40).flexGrow(1)
  71.             Button('清除选区')
  72.               .onClick(() => {
  73.                 this.textController.setTextSelection(0, 0);
  74.                 this.selectionLogs = '选区已被重置清空';
  75.               })
  76.               .backgroundColor('#2C2C2C').fontColor('#FFFFFF').height(40)
  77.           }
  78.           .width('100%').margin({ bottom: 12 })
  79.           Text(this.selectionLogs)
  80.             .fontSize(12).fontColor('#888888').lineHeight(16).width('100%')
  81.         }
  82.         .width('100%').padding(16).backgroundColor('#161616').borderRadius(10)
  83.         .border({ width: 1, color: '#2C2C2C' })
  84.       }
  85.       .width('100%').flexGrow(1).padding(16).backgroundColor('#000000')
  86.     }
  87.     .height('100%').width('100%').backgroundColor('#000000')
  88.   }
  89. }
复制代码

四、运行效果与性能
在实际设备测试中,设置起始索引0、结束索引10并触发后,文本“HarmonyOS ”立即被蓝色高亮覆盖,同时弹出系统原生复制/全选菜单。即使以30Hz频率快速触发,CPU占用率仍维持在个位数,无任何卡顿或重排抖动。索引超出文本长度时,底层自动截断至边界,选区顺滑贴合末尾字符。

五、避坑指南
1. CopyOptions.None导致静默失效:如果忘记设置copyOption或错误设为None,setTextSelection调用后无任何视觉反馈。务必显式设置:
  1. .copyOption(CopyOptions.InApp) // 或 CopyOptions.LocalDevice
复制代码
2. 不可见截断区域导致选择哑弹:当Text组件因尺寸限制或textOverflow截断导致部分字符不可见时,选中不可见区域不会触发高亮。如需高亮超出边缘的文本,应关闭截断或使用滚动容器确保所有字符在排版树上可见。

六、总结
setTextSelection()为鸿蒙应用带来了原生级别的动态选区控制能力,彻底解放了长按手势的束缚。通过将高亮渲染下沉至底层文本引擎,实现了零重排的高性能交互,并完美兼容系统级菜单。该接口非常适合AI划词、动态标注、语音朗读同步高亮、全文搜索关键词高亮等场景,建议开发者在API 23及以上版本的项目中积极采用。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: TextController动态文本选择高亮实战:HarmonyOS NEXT 6.1新特性解析

感谢分享!这个 `setTextSelection()` 接口确实很实用,以前只能靠用户手势,现在可以代码控制高亮,对做 AI 划词、语音同步标注这类功能太有帮助了。有个小疑问:如果文本组件设置了多种 fontColor 或富文本样式(比如通过 Span),高亮覆盖后原来的颜色会被覆盖吗?还是说蓝色高亮是半透明叠加的?期待后续更多实践分享。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-4 20:14 , Processed in 0.024458 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部