☰
HarmonyOS 7 文本选区交互:点到别处不消失,setTextSelectionClearPolicy 该怎么用
2026/9/28 21:23:04 网站建设 项目流程

HarmonyOS 7 文本选区交互:点到别处不消失,setTextSelectionClearPolicy 该怎么用

在阅读页长按选了一段话,接着点旁边的留白,蓝色选区和两个手柄还在。换到临时信息面板,又希望点到外面就结束选中。两种需求都合理,但如果每个页面随手设置一次,返回阅读页后很容易留下另一种交互。

HarmonyOS 7 的 API 26 新增 setTextSelectionClearPolicy,给出了明确的文本选择清除策略。这里先做一个能对照观察的页面,再把临时策略的进入、退出管理起来。重点不是强制所有页面都清除,而是让一处设置的影响有明确归属。

先纠正一个容易看错的词:外部

官方枚举说的是“触摸到文本组件外部”,不是“点到当前选区以外的任何地方”。一段长文本中没有被选中的字,仍可能位于同一个 Text 组件内。测试时把这两种位置混在一起,就会把正确结果当成接口失效。

策略外部触摸后的文本选择默认情况
KEEP_SELECTED_TEXT_ON_EXTERNAL_TOUCH保留选中与手柄未设置时采用此策略
CLEAR_SELECTED_TEXT_ON_EXTERNAL_TOUCH清除选中与手柄需要显式设置

清除选中不等于删除正文,也不是清空剪贴板,更不是一条通用的焦点重置命令。已经复制出去的内容不会因为换了这个策略就获得额外的安全保护。如果需求是敏感信息不可复制,需要另行设计复制权限和内容呈现,不应借用一个交互策略来实现。

本文核对的 UIContext 文档更新于2026年9月23日,核对日期为9月27日。接口及枚举从26.0.0开始支持,仅用于Stage模型,系统能力为 SystemCapability.ArkUI.ArkUI.Full;API26同时支持元服务。UIContext模块本身更早存在,不能把模块的起始版本当作这个方法的起始版本。

图中下半部分的策略栈是应用自己实现的管理方式,不是系统提供了自动入栈、出栈接口。恢复 KEEP 仅恢复后续交互策略,不会把已经清掉的选区重新选回来。

案例一:把“看起来没生效”变成可重复的对照实验

先不接业务,把下面页面放进API26 Stage工程的页面目录并注册到页面配置。两个按钮只切换策略;中间Text可以选择,底部留白区则明确位于Text组件之外。切换策略后重新长按选择文字,再触摸留白区,不要拿按钮点击前遗留的状态判断结果。

import { TextSelectionClearPolicy } from '@kit.ArkUI'; @Entry @Component struct SelectionLab { @State policyName: string = 'KEEP'; private apply(clear: boolean): void { this.getUIContext().setTextSelectionClearPolicy(clear ? TextSelectionClearPolicy.CLEAR_SELECTED_TEXT_ON_EXTERNAL_TOUCH : TextSelectionClearPolicy.KEEP_SELECTED_TEXT_ON_EXTERNAL_TOUCH); this.policyName = clear ? 'CLEAR' : 'KEEP'; } build() { Column({ space: 18 }) { Text('当前策略:' + this.policyName).fontSize(18) Row({ space: 12 }) { Button('保留选区').onClick(() => this.apply(false)) Button('外部触摸清除').onClick(() => this.apply(true)) } Text('这段文字可用于选择实验。先长按选中几个字,再触摸下面的留白区域。') .fontSize(22) .copyOption(CopyOptions.LocalDevice) .width('100%') .padding(12) .backgroundColor('#EDF3FF') Column() { Text('文本组件外部的测试区域').fontSize(14) } .width('100%') .height(180) .backgroundColor('#F2F2F2') } .padding(24) .width('100%') .height('100%') } }

这里的 LocalDevice 来自 Text 的复制配置,作用是让Text具备相应复制选择交互,不是清除策略本身。不要把一个未配置可选择能力、根本无法建立选区的Text当成CLEAR成功案例。

按以下顺序验证,一次只改变一个变量:

  1. 初次进入,长按建立选区,触摸下方留白。观察默认KEEP。
  2. 点击“外部触摸清除”,重新建立选区,触摸同一留白。观察CLEAR。
  3. 再设置KEEP,再次建立选区,重复相同操作。
  4. 对比触摸Text内部未选中的字与Text外部留白;分别记录,不合并成一个结论。

观察点包括选中背景、两端手柄、正文内容,以及触摸位置是否真的越过组件边界。正文前后应相同。为了定位布局问题,可以给组件临时添加明显背景,但不要添加会吞掉触摸事件的业务手势后才做第一次测试。

此处提供的是按官方签名编写的ArkUI接入代码;当前验证环境没有API26 SDK和可连接真机,未完成该页面的API26编译及设备交互验证。官方UIContext参考也提示效果以真机为准,不能把预览器无效果直接判为平台缺陷。下文实际执行的是公开策略模型及断言,不是这一段UI代码的设备测试。

案例二:临时面板关了,阅读页为什么仍然自动清除

假设阅读页需要KEEP,临时面板需要CLEAR。最直接的代码是在进入面板时设CLEAR,退出时设KEEP。单层场景能工作,但以后又出现第二个面板,就容易出现较早面板退出时覆盖较新面板策略的情况。

不能只把“退出恢复默认值”写在每个页面的消失回调里。默认值不一定等于上一个活动场景的要求,而且组件存在也不等于页面当前可见。缓存页面、Navigation目的页、弹层和子窗的可见生命周期需要由应用入口协调。

我会选一个很小的所有者列表:每次进入返回唯一token;退出仅移除自己的token;最后进入且仍有效的所有者决定当前策略。基础策略由应用明确传入,而不是读取一个本文没有验证存在的系统getter。

export type SelectionPolicy = 'KEEP' | 'CLEAR'; interface PolicyOwner { token: number; name: string; policy: SelectionPolicy; } export class SelectionPolicyScope { private nextToken: number = 0; private owners: PolicyOwner[] = []; private base: SelectionPolicy; private apply: (policy: SelectionPolicy) => void; constructor( base: SelectionPolicy, apply: (policy: SelectionPolicy) => void ) { this.base = base; this.apply = apply; this.apply(base); } current(): SelectionPolicy { return this.owners.length === 0 ? this.base : this.owners[this.owners.length - 1].policy; } enter(name: string, policy: SelectionPolicy): number { const owner: PolicyOwner = { token: ++this.nextToken, name, policy }; this.owners.push(owner); try { this.apply(this.current()); } catch (error) { this.owners.pop(); throw error; } return owner.token; } leave(token: number): boolean { const index = this.owners.findIndex(owner => owner.token === token); if (index < 0) return false; const removed: PolicyOwner = this.owners[index]; this.owners.splice(index, 1); try { this.apply(this.current()); } catch (error) { this.owners.splice(index, 0, removed); throw error; } return true; } }

enter/leave是本例自定义方法,与系统接口没有同名对应关系。列表按“最近进入优先”工作,是这里选择的产品规则;若应用要求显式优先级,应该改变规则并补测试,不能暗示框架原本就有这个优先级。

应用只在一个位置把字符串策略映射到平台枚举:

import { UIContext, TextSelectionClearPolicy } from '@kit.ArkUI'; import { SelectionPolicyScope, SelectionPolicy } from './SelectionPolicyScope'; export function createSelectionScope(ui: UIContext): SelectionPolicyScope { return new SelectionPolicyScope('KEEP', (policy: SelectionPolicy): void => { ui.setTextSelectionClearPolicy(policy === 'KEEP' ? TextSelectionClearPolicy.KEEP_SELECTED_TEXT_ON_EXTERNAL_TOUCH : TextSelectionClearPolicy.CLEAR_SELECTED_TEXT_ON_EXTERNAL_TOUCH); }); }

一个UIContext对应一份这样的管理对象,由对应窗口或页面容器持有。不要把第一个窗口的UIContext存成跨窗口全局单例,再拿去改第二个窗口。调用示意是进入可见阅读场景时enter,打开面板时再enter,真正结束该场景时leave;这些动作接到哪一种生命周期,取决于实际使用的路由和弹层,而不是统一塞进aboutToDisappear就算结束。

用两种退出顺序检查是否相互覆盖

下面代码与上面的模型放在同一TypeScript文件可以直接执行。第一组覆盖正常嵌套退出;第二组故意让较早所有者先退出,并重复退出旧token。它们验证的是应用管理规则,不能替代真机文本选择行为测试。

function expectPolicy(actual: SelectionPolicy, expected: SelectionPolicy): void { if (actual !== expected) throw new Error(actual + ' != ' + expected); } let applied: SelectionPolicy = 'KEEP'; const scope = new SelectionPolicyScope('KEEP', policy => { applied = policy; }); const reader = scope.enter('reader', 'KEEP'); const panel = scope.enter('panel', 'CLEAR'); expectPolicy(applied, 'CLEAR'); scope.leave(panel); expectPolicy(applied, 'KEEP'); scope.leave(reader); expectPolicy(applied, 'KEEP'); const oldPanel = scope.enter('old-panel', 'CLEAR'); const newReader = scope.enter('new-reader', 'KEEP'); scope.leave(oldPanel); expectPolicy(applied, 'KEEP'); if (scope.leave(oldPanel)) throw new Error('old token must not be reused'); scope.leave(newReader); expectPolicy(applied, 'KEEP'); console.log('nested and out-of-order policy cases passed');

再增加一次平台适配器抛错的测试,避免模型声称进入成功而平台设置失败:

let fail: boolean = false; const failureScope = new SelectionPolicyScope('KEEP', () => { if (fail) throw new Error('simulated adapter failure'); }); fail = true; let rejected: boolean = false; try { failureScope.enter('failed-panel', 'CLEAR'); } catch { rejected = true; } if (!rejected) throw new Error('failure was swallowed'); expectPolicy(failureScope.current(), 'KEEP'); console.log('adapter failure rollback passed');

这是内存模型回退,不是承诺系统调用出错后一定没有任何副作用。适配器异常仍应交由上层记录并处理;不要用空catch把错误藏起来,也不要无限重试一个已经失效的UIContext。

简单设置和统一管理,什么时候选哪一种

只有一个固定阅读页、策略从不切换时,直接设置一次就够。引入所有者列表反而增加维护点。出现多个会临时改变策略的场景后,统一入口的价值才体现出来:查日志能知道当前是谁持有CLEAR,而不是在十几个页面里搜索最后一次调用。

方案适用情况要承担的成本
固定场景直接设置单页面、没有临时覆盖后续新增场景时重新评估
各页面退出时写回KEEP严格单层且基础需求永远KEEP嵌套与乱序退出容易覆盖其他场景
每UIContext一个所有者管理器多场景临时切换、存在嵌套必须准确对接可见生命周期并释放token

不要为了让选区消失去重建整棵Text节点,也不要把触摸事件全局拦截来模拟CLEAR。前者增加不必要的状态变化,后者可能影响滚动、链接和无障碍交互。官方明确提供了这一策略时,优先采用它,再把应用侧的归属管理好。

接入后我会保留四项回归:KEEP与CLEAR分别触摸组件外部;嵌套场景按两种顺序退出;两个窗口分别改变策略;页面缓存后返回。每一项都把触摸位置、当前UIContext、持有者和期望策略记在一起。问题再发生时,才能区分接口行为、布局边界和应用状态覆盖,而不是继续增加一次“恢复默认”的调用。

官方参考

  • UIContext.setTextSelectionClearPolicy:起始版本、默认策略、示例
  • TextSelectionClearPolicy:外部触摸的两种行为
  • UIContext:获取方式与Stage模型限制

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询