【HarmonyOS 7 闪控窗深度实战】05 闪控窗处理拖动、侧边暂存与关闭状态
前言
用户在其他页面继续处理事情时,闪控窗可能挡住正在阅读的内容。拖动可以改变窗口位置,暂时收起可以让出更多空间,而关闭则结束这一次悬浮查看。这些动作针对的是显示入口,文件整理本身可能仍在进行。如果应用把窗口消失直接理解为任务取消,用户只是整理屏幕布局,就可能意外结束正在等待的工作。
窗口不可见之后,下一次操作也取决于它去了哪里。隐藏的窗口可能继续保留,侧边栏中的入口可以恢复原窗口,已经停止的窗口则需要结束旧实例后重新创建。因此,一个表示是否显示的布尔值无法说明全部后续行为。应用需要知道系统报告了哪种状态,再决定是否保留控制器,不能先丢弃引用再尝试解释消失原因。
HarmonyOS 7 通过状态与矩形回调通知应用窗口发生了什么变化。应用可以根据这些通知保存窗口信息,并决定是否继续保留控制器。任务是否暂停、完成或取消仍由业务对象管理,窗口回调不会替文件整理生成处理结果。
窗口事件与任务事件还可能交错发生。拖动过程中会出现多条位置记录,任务计时器同时继续增加百分比;窗口暂存期间任务可能完成,恢复后就需要显示完成结果。工程如果在每次窗口显示时重新开始任务,或者在每次不可见时停止任务,就会把两条过程错误地绑在一起。当前实现需要让矩形变化更新窗口信息,让状态变化管理窗口,再由共享存储向页面提供业务现状。
当前应用在主页面分别显示任务进度、窗口状态和停止原因。用户拖动、隐藏或暂存窗口时,任务对象继续按原规则更新进度;窗口停止以后,应用清理窗口资源。这个实现只处理内存中的本地任务,没有接入闪控球绑定或长期后台执行,控制器是否保留需要由实际窗口状态决定。

一、不可见状态决定控制器是否需要继续保留
窗口消失以后,应用先等待系统状态说明发生了什么,任务对象保持原来的业务处理。侧边入口可能仍持有原窗口,此时立即清空引用并创建新实例,会使应用失去对旧窗口的管理。因此,当前实现把状态识别放在清理之前,每个分支只处理它能够确认的结果。
HIDDEN 表示窗口隐藏,IN_SIDEBAR 表示进入侧边栏,STOPPED 表示窗口已经停止。这三种状态都可能让用户暂时看不到完整内容,但后续操作不同。隐藏或暂存时,应用不应立即释放控制器并重新创建窗口;停止以后则需要按停止结果清理引用。
任务完成可能发生在窗口显示或暂存期间,窗口关闭也可能发生在任务尚未结束时。应用因此分别保存窗口状态和任务状态,让两组信息各自描述真实对象。窗口回调更新显示状态,任务方法更新业务结果,系统事件就不会意外覆盖整理进度。
窗口状态还包括 STARTED、IN_FLOATING_BALL 和 ERROR。应用需要能够识别这些名称,但不同状态并不采用同一种处理方式。表格把状态含义与当前处理动作列在一起,开发者可以据此判断哪些状态保留控制器,哪些状态进入清理。
| 窗口状态 | 含义 | 当前处理 |
|---|---|---|
STARTED | 已启动并显示 | 保留控制器,页面读取当前任务 |
HIDDEN | 已隐藏 | 保留控制器 |
IN_SIDEBAR | 在侧边栏中 | 保留控制器,等待用户恢复 |
STOPPED | 已停止 | 解除监听并清理引用 |
IN_FLOATING_BALL | 已切换为闪控球 | 当前没有接入绑定 |
ERROR | 窗口发生异常 | 保留状态识别,异常交互仍待确认 |
隐藏和侧边暂存都保留原窗口,应用不能把它们统一处理成停止。STOPPED 才表示这一轮窗口已经结束,应用随后需要解除监听并清理引用。当前没有接入闪控球绑定,异常状态也没有完整运行结果,因此状态名称齐全不代表所有交互都已覆盖。
当前代码通过显式名称列表显示窗口状态,没有依赖枚举反向索引。列表位置与 API 26 的枚举数值对应,页面据此显示可读名称。未知数值会使用 UNKNOWN,应用不会把无法识别的状态直接归为停止。
const stateNames: string[] = ['UNKNOWN', 'STARTED', 'HIDDEN', 'STOPPED', 'IN_SIDEBAR', 'IN_FLOATING_BALL', 'ERROR'];
this.windowState = stateNames[info.state] ?? 'UNKNOWN';
this.stopReason = info.stopReason;
平台版本变化以后,开发者需要重新核对状态名称与数值的对应关系。如果出现未知状态,应用可以保留原始回调用于排查,同时避免直接清理业务任务。这个处理让名称显示失败与窗口停止保持区别。
状态也不能仅从主页面文字反推。应用先发出隐藏请求,随后才收到系统状态,文字应该反映回调结果;如果请求失败,直接把文字改成隐藏会造成界面与系统不符。当前显示与隐藏调用捕获错误,窗口名称由状态监听更新,因此开发者能够区分请求已经发出和窗口已经改变。相同原则也适用于停止操作,Promise 返回和终止回调分别记录。
二、拖动、隐藏与侧边暂存改变窗口入口
控制器的保留规则确定以后,位置变化与可见状态就可以分别处理。拖动改变窗口矩形,隐藏与侧边暂存改变用户访问窗口的方式,它们都没有向任务对象提交新的业务命令。矩形回调因此只更新窗口信息,任务计时器继续按原规则执行。
用户拖动窗口时,应用需要观察位置变化,任务状态没有因此改变。当前 onRectChange() 保存变化原因与坐标,并继续读取窗口属性。拖动中可能连续收到多个回调,因此日志中的多次位置变化不表示窗口被反复创建。
console.info('FloatTask05 rect=' + JSON.stringify(info));
this.lastRect = info.reason + ' ' + info.windowRect.left + ',' + info.windowRect.top;
this.readProperties();
POSITION_CHANGE 表示位置变化,SIZE_CHANGE 表示大小变化,RECT_CHANGE 表示位置和大小同时变化。开发者需要按回调内容分析,不能把所有矩形事件都归为用户拖动。启动、调整尺寸或系统修正也可能影响矩形。
窗口拖动以后,onRectChange() 回调会保存新的位置信息,原任务仍然读取和写入同一份存储。当前回调没有调用暂停或开始方法,所以移动窗口不会创建新的整理任务。位置数据也没有被持久化为下次启动坐标,用户重新创建窗口时仍由系统安排显示位置。
系统提供窗口拖动区域,应用自己的滚动容器和按钮则处理内容交互。用户从标题区域拖动窗口,与从内容区域滚动页面,操作对象不同。开发者排查窗口没有移动时,需要先确认手势进入了哪个区域,再检查矩形回调。
当前主页面显示最近一次窗口坐标,便于在开发时判断移动结果。产品页面可以不展示这些数值,但矩形回调仍然可以用于布局或问题排查。应用如果长期记录密集的位置变化,需要控制日志输出量,避免保留大量没有业务用途的信息。
窗口到达屏幕边缘以后,系统可能提供额外的暂存交互,具体动作仍应由状态回调确认。应用没有在某个坐标接近屏幕边缘时自行写入 IN_SIDEBAR,因为坐标并不能完整表达系统侧边栏状态。开发者设计拖动后的业务反馈时,应当先等待系统报告,再决定是否展示提示,避免窗口仍在屏幕内却已经显示暂存状态。
用户点击标题栏最小化以后,当前未绑定闪控球的窗口进入侧边栏,回调变为 IN_SIDEBAR。应用保留控制器,文件整理继续更新存储。用户点击侧边入口,原窗口重新显示并收到 STARTED,内容读取到继续推进后的进度。

前台隐藏与侧边暂存使用不同的显示方式。侧边暂存适用于 Phone 非自由多窗模式,以及 Tablet 非自由多窗且非 PC 模式。用户可以从系统侧边入口恢复暂存窗口,前台隐藏则由应用的显隐设置控制。应用需要分别处理这两个入口,同时为它们保留原控制器。
应用在前台调用 setFloatViewVisibilityInApp(false) 后,窗口会隐藏并报告 HIDDEN。应用恢复可见设置以后,原窗口再次显示,不需要重新创建。这个接口控制的是应用位于前台时的可见性,所以隐藏操作不能直接理解为窗口已经停止。
try { await this.controller?.setFloatViewVisibilityInApp(visible); }
catch (error) { console.error('FloatTask05 visibility=' + (error as BusinessError).code); }
应用退到后台以后,窗口仍然可能继续显示,因为该设置约束的是前台场景。开发者如果希望结束窗口,需要使用停止流程并等待 STOPPED。任务对象也不会因为前台隐藏设置变化而自动暂停,业务操作仍然由任务方法决定。
窗口状态回调不会调用任务对象的 pause() 或 cancel()。用户只是改变显示方式,业务没有收到暂停或取消命令,因此计时器继续执行。这项设计适用于需要持续显示进度的本地任务,其他业务如果确实要求窗口关闭即结束,也应该写成明确产品规则。

从侧边恢复以后,窗口内容继续读取现有任务存储,因此用户可能看到比暂存时更大的百分比。这个差异符合任务仍在执行的预期,不需要把数值回退到暂存时的快照。应用如果保存了侧边操作发生时的页面副本,并在恢复时重新写回,就会覆盖后来完成的工作。当前只保留控制器与任务对象,恢复显示不执行这种快照回写。
暂存期间任务也可能完成。用户恢复窗口时应当直接看到完成结果,不能仅因刚刚恢复显示就把任务状态改成运行。这个条件说明状态分离还影响最终结果展示:窗口从暂存变成显示,任务可能从运行变成完成,两条变化线可以在不同时间推进。当前代码没有在 STARTED 中调用任务开始,所以重复显示不会重新启动已经完成的整理。
是否自动恢复窗口也需要尊重用户操作。当前应用让用户点击系统侧边入口恢复,不在任务每次更新时把窗口拉回屏幕。用户主动暂存通常是为了让出视线,进度变化并没有自动撤销这个意图。真实业务若需要完成提醒,应当按产品规则设计提醒方式,不能把普通进度更新都变成窗口再次出现的理由。
三、收到 STOPPED 以后再结束这一轮窗口
隐藏和暂存都保留原窗口,收到停止状态以后,应用才解除监听并清理引用。停止请求本身仍处于异步过程,方法返回不能代替终止状态。因此,当前清理以系统回调为终点,旧引用尚未释放时不进入新一轮创建。
窗口停止时,stopReason 才具有停止原因的含义。应用主动调用停止、点击标题栏关闭、从侧边栏关闭以及主窗口销毁,可能对应不同原因。开发者保存原因,才能分清正常用户关闭与需要调查的意外退出。
用户点击标题栏关闭按钮以后,系统报告 STOPPED,停止原因是 TITLE_BAR_STOP_CLICK。应用清理窗口引用,但没有调用任务取消,主页面仍然可以显示原任务结果。用户再次请求显示时,新的窗口也会连接原存储。
if (info.state === floatView.FloatViewState.STOPPED) {
controller.offRectChange();
controller.offLimitsChange();
controller.offStateChange();
if (this.controller === controller) { this.controller = undefined; }
}
监听清理需要与创建时的注册位置对照。每次创建注册了哪些监听,停止时就需要有对应结束动作,否则旧回调可能继续影响页面。当前代码已经列出这组解除调用,但具体 API 仍可能抛出异常,因此调用位置齐全只能说明正常清理路径存在,不能证明所有失败路径都已结束。
清理监听使用创建时捕获的控制器,引用比较防止旧回调清除其他控制器。当前应用仍只有一个窗口实例,但异步停止与重新创建会让对象身份成为需要关注的条件。控制器释放以后,窗口页面可以重建,任务存储则继续由业务对象维护。
监听解除本身也可能抛出异常,所以应用仍然需要区分正常清理与清理失败。当前停止分支列出了正常解除路径,没有保证每个异常发生后都能完成全部释放。开发者完善清理时,需要分别处理各监听的异常,防止前一个解除失败阻断后面的操作。
停止原因保留下来以后,应用可以区分用户主动关闭与非预期退出。当前不会收到关闭事件就自动拉起窗口,因为用户刚刚作出的关闭选择仍然有效。产品确实需要再次提醒时,需要由明确业务规则发起显示请求,普通进度更新没有撤销用户选择的含义。
停止原因也不能直接作为文件任务结果。例如主窗口销毁使闪控窗停止,只能说明关联窗口结束,不能证明文件整理已经完成。应用需要通过自己的任务状态、实际产物和恢复记录判断业务结果,不能从窗口系统事件推导成功。
不同关闭入口对应不同停止原因,开发者排查时需要保留系统返回的原始字符串。应用按钮、标题栏和侧边入口发生操作时,窗口所处状态并不相同。原始原因能够帮助应用定位关闭来源,也避免把所有停止都当作用户点击了同一个按钮。
窗口停止时还可能有任务更新正在等待执行。当前停止分支只处理窗口监听,不清理业务定时器,所以后续任务回调仍然写入原有存储。主页面继续观察该存储,用户因此能够留在应用中确认结果。这个行为属于当前应用的明确选择,开发者接入具有窗口依赖的业务时,需要重新检查业务能否脱离窗口继续执行,不能机械套用同一处理。
清理后的再次创建需要等待停止确认。用户快速连续点击停止与显示时,旧控制器尚未清空,显示方法会因为已有引用而返回;收到终止状态以后,用户才能创建下一轮窗口。这个等待避免了把一个异步停止请求当成已经释放所有资源。当前没有自动排队显示请求,因此快速点击被忽略后,需要用户在停止完成以后再次点击。
停止原因只有在终止状态中具有意义,所以应用不应该拿显示状态中的空字符串判断发生了未知故障。当前每次回调都更新页面字段,正常显示时停止原因为空,停止后才保存系统返回内容。开发者如果希望持续展示最近一次关闭原因,可以另外保存历史字段,不能把历史原因混成当前状态,误导用户认为一个正在显示的窗口已经被关闭。
HIDDEN 也不能单独说明用户执行了哪一种操作。应用前台隐藏设置和系统最近任务界面都可能涉及隐藏,状态名称只描述结果。开发者调查异常显隐时,需要结合触发入口与前台条件判断,不能仅根据名称认定控制器已经失效。
如果窗口状态保持显示而页面文字停止变化,开发者应该回到任务状态源检查;如果任务日志持续推进而系统返回停止,则应该检查窗口重建入口。这种排查分工来自两个对象的实际职责,能够让问题落到具体代码位置。把所有现象统一称为窗口显示异常,会使计时器停止、页面绑定丢失和窗口终止混在一起,难以决定下一步应该观察什么。
四、窗口已经不可见时仍要保留正确的任务结果
用户暂存窗口期间,任务仍可能从运行进入完成。应用再次显示窗口时,应当直接读取完成结果,不能因为窗口重新出现就把业务状态改回运行。这个场景要求窗口恢复只处理显示连接,原任务结果继续由运行时保存。
用户关闭窗口以后,主页面仍然保留任务入口,用户可以继续查看或操作。当前窗口停止分支没有清理业务计时器,因此关闭窗口不会主动取消整理。不过主页面销毁和进程结束仍然影响内存任务,闪控窗本身没有提供长期后台执行或持久化恢复保证。
当前没有绑定闪控球,也没有完整验证系统异常、拖入垃圾桶、其他窗口挤占或主窗口销毁等停止路径。应用可以识别这些状态和原因,具体异常处理仍需结合对应运行条件确认。开发者扩展时需要保持同一条业务边界:窗口事件不能直接替代任务结果。
总结
当前应用已经能够识别窗口显示、隐藏、侧边暂存和停止,并记录拖动后的矩形与关闭原因。任务状态独立保留,窗口暂存和停止不会主动调用任务取消。控制器是否释放由 STOPPED 确认,任务是否完成继续由业务结果决定,因此重新显示时仍能读取原来的处理结果。
用户从侧边入口恢复窗口后,可以继续查看原任务;用户需要完整操作时,还需要能够返回应用中的任务详情。这个返回过程既要恢复所属主窗口,也要把当前任务标识交给应用。任务数据已经独立保存,页面定位就可以只传标识,再从运行时读取最新结果。
本专栏主要通过 HarmonyOS 7 模拟器进行验证,真机上的系统表现、设备差异和实际体验,后面有条件再继续补测,最终还是以实际设备运行结果为准。
完整代码
FloatViewPage.ets
import { runtime } from './Index';
@Entry({ useSharedStorage: true })
@Component
struct FloatViewPage {
@LocalStorageLink('taskState') taskState: string = '尚未开始';
@LocalStorageLink('progress') progress: number = 0;
@LocalStorageLink('compact') compact: boolean = false;
@LocalStorageLink('topInset') topInset: number = 48;
@LocalStorageLink('bottomInset') bottomInset: number = 24;
build() {
Scroll() {
Column({ space: 8 }) {
if (!this.compact) { Text('文件整理').fontSize(22).fontWeight(FontWeight.Bold) }
Text(this.taskState + ' ' + this.progress + '%').fontSize(this.compact ? 12 : 20)
if (!this.compact) {
Progress({ value: this.progress, total: 100 }).width('80%')
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Center }) {
Button('暂停').enabled(this.taskState === '正在整理文件').onClick(() => runtime.pause()).margin(4)
Button('恢复').enabled(this.taskState === '已暂停').onClick(() => runtime.resume()).margin(4)
Button('取消').enabled(this.taskState === '正在整理文件' || this.taskState === '已暂停')
.onClick(() => runtime.cancel()).margin(4)
}
}
}.width('100%').padding({ top: this.topInset, bottom: this.bottomInset, left: 12, right: 12 })
}.width('100%').height('100%')
}
}
Index.ets
import { floatView, window } from '@kit.ArkUI';
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
class TaskRuntime {
readonly storage: LocalStorage = new LocalStorage();
private timer: number = -1;
constructor() {
this.storage.setOrCreate<string>('taskState', '尚未开始');
this.storage.setOrCreate<number>('progress', 0);
this.storage.setOrCreate<boolean>('compact', false);
this.storage.setOrCreate<number>('topInset', 48);
this.storage.setOrCreate<number>('bottomInset', 24);
}
start(): void {
if (this.timer !== -1 || this.storage.get<string>('taskState') === '已暂停') { return; }
this.storage.set<number>('progress', 0);
this.storage.set<string>('taskState', '正在整理文件');
this.startTimer();
}
pause(): void {
if (this.storage.get<string>('taskState') !== '正在整理文件') { return; }
this.dispose();
this.storage.set<string>('taskState', '已暂停');
console.info('FloatTask05 action=pause progress=' + this.storage.get<number>('progress'));
}
resume(): void {
if (this.storage.get<string>('taskState') !== '已暂停') { return; }
this.storage.set<string>('taskState', '正在整理文件');
this.startTimer();
}
cancel(): void {
const state = this.storage.get<string>('taskState');
if (state !== '正在整理文件' && state !== '已暂停') { return; }
this.dispose();
this.storage.set<string>('taskState', '已取消');
console.info('FloatTask05 action=cancel progress=' + this.storage.get<number>('progress'));
}
private startTimer(): void {
if (this.timer !== -1) { return; }
this.timer = setInterval(() => {
const progress = Math.min(100, (this.storage.get<number>('progress') ?? 0) + 1);
this.storage.set<number>('progress', progress);
console.info('FloatTask05 progress=' + progress);
if (progress === 100) {
this.storage.set<string>('taskState', '文件整理完成');
this.dispose();
}
}, 1000);
}
dispose(): void {
if (this.timer !== -1) { clearInterval(this.timer); this.timer = -1; }
}
}
export const runtime: TaskRuntime = new TaskRuntime();
@Entry(runtime.storage)
@Component
struct Index {
@LocalStorageLink('taskState') taskState: string = '尚未开始';
@LocalStorageLink('progress') progress: number = 0;
@State windowState: string = '尚未启动';
@State supported: boolean = false;
@State busy: boolean = false;
@State stopReason: string = '';
@State lastRect: string = '';
private template: floatView.FloatViewTemplateType = floatView.FloatViewTemplateType.ROUNDED_RECTANGLE;
private controller: floatView.FloatViewController | undefined = undefined;
aboutToAppear(): void {
this.supported = canIUse('SystemCapability.Window.SessionManager') && floatView.isFloatViewEnabled();
console.info('FloatTask05 capability=' + this.supported);
}
async openFloat(): Promise<void> {
if (this.busy || this.controller) { return; }
if (!this.supported) { this.windowState = '当前设备不支持闪控窗'; return; }
this.busy = true;
try {
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
const result = await abilityAccessCtrl.createAtManager().requestPermissionsFromUser(context,
['ohos.permission.FLOAT_VIEW']);
console.info('FloatTask05 permission=' + JSON.stringify(result.authResults));
if (result.authResults[0] !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
this.windowState = '闪控窗权限未授权'; return;
}
const controller = await floatView.create({ context: context,
templateType: floatView.FloatViewTemplateType.ROUNDED_RECTANGLE });
this.controller = controller;
this.template = floatView.FloatViewTemplateType.ROUNDED_RECTANGLE;
runtime.storage.set<boolean>('compact', false);
controller.onRectChange((info: floatView.FloatViewRectChangeInfo) => {
console.info('FloatTask05 rect=' + JSON.stringify(info));
this.lastRect = info.reason + ' ' + info.windowRect.left + ',' + info.windowRect.top;
this.readProperties();
});
controller.onLimitsChange((limits: floatView.FloatViewLimits) => {
console.info('FloatTask05 changedLimits=' + JSON.stringify(limits));
});
controller.onStateChange((info: floatView.FloatViewStateChangeInfo) => {
const stateNames: string[] = ['UNKNOWN', 'STARTED', 'HIDDEN', 'STOPPED', 'IN_SIDEBAR', 'IN_FLOATING_BALL', 'ERROR'];
this.windowState = stateNames[info.state] ?? 'UNKNOWN';
this.stopReason = info.stopReason;
console.info('FloatTask05 state=' + JSON.stringify(info));
if (info.state === floatView.FloatViewState.STARTED) { this.readProperties(); }
if (info.state === floatView.FloatViewState.STOPPED) {
controller.offRectChange();
controller.offLimitsChange();
controller.offStateChange();
if (this.controller === controller) { this.controller = undefined; }
}
});
await controller.setUIContext('pages/FloatViewPage', runtime.storage);
const limits = floatView.getFloatViewLimits(floatView.FloatViewTemplateType.ROUNDED_RECTANGLE);
console.info('FloatTask05 limits=' + JSON.stringify(limits));
await controller.setWindowSize(limits.maxSize);
await controller.start();
} catch (error) {
const err = error as BusinessError;
this.windowState = '启动失败:' + err.code;
console.error('FloatTask05 error=' + err.code + ':' + err.message);
this.controller?.offStateChange();
this.controller = undefined;
} finally { this.busy = false; }
}
readProperties(): void {
if (!this.controller) { return; }
try {
const props = this.controller.getWindowProperties();
console.info('FloatTask05 properties=' + JSON.stringify(props));
runtime.storage.set<number>('topInset', Math.max(12, this.getUIContext().px2vp(props.avoidArea.topRect.height)));
runtime.storage.set<number>('bottomInset', Math.max(16, this.getUIContext().px2vp(props.avoidArea.bottomRect.height)));
} catch (error) { console.info('FloatTask05 propertiesPending=' + (error as BusinessError).code); }
}
async resize(small: boolean): Promise<void> {
if (!this.controller) { return; }
try {
const limits = floatView.getFloatViewLimits(this.template);
const size: window.Size = small ? limits.minSize : limits.maxSize;
runtime.storage.set<boolean>('compact', small || this.template === floatView.FloatViewTemplateType.HORIZONTAL_BAR);
console.info('FloatTask05 request=' + JSON.stringify(size));
await this.controller.setWindowSize(size);
} catch (error) { console.error('FloatTask05 resize=' + (error as BusinessError).code); }
}
async changeTemplate(): Promise<void> {
if (!this.controller) { return; }
try {
const next = this.template === floatView.FloatViewTemplateType.ROUNDED_RECTANGLE ?
floatView.FloatViewTemplateType.HORIZONTAL_BAR : floatView.FloatViewTemplateType.ROUNDED_RECTANGLE;
const limits = floatView.getFloatViewLimits(next);
await this.controller.switchTemplate({ templateType: next, size: limits.maxSize });
this.template = next;
runtime.storage.set<boolean>('compact', next === floatView.FloatViewTemplateType.HORIZONTAL_BAR);
console.info('FloatTask05 template=' + next + ' requested=' + JSON.stringify(limits.maxSize));
} catch (error) { console.error('FloatTask05 templateError=' + (error as BusinessError).code); }
}
async setVisible(visible: boolean): Promise<void> {
try { await this.controller?.setFloatViewVisibilityInApp(visible); }
catch (error) { console.error('FloatTask05 visibility=' + (error as BusinessError).code); }
}
async closeFloat(): Promise<void> {
if (!this.controller || this.busy) { return; }
try { await this.controller.stop(); }
catch (error) {
console.error('FloatTask05 stop=' + (error as BusinessError).message);
}
}
aboutToDisappear(): void {
runtime.dispose();
}
build() {
Column({ space: 4 }) {
Text('文件整理').fontSize(30).fontWeight(FontWeight.Bold)
Text('阶段 05 · 窗口状态管理').fontSize(18)
Text(this.taskState).fontSize(24)
Text(this.progress + '%').fontSize(32)
Progress({ value: this.progress, total: 100 }).width(260)
Text('闪控窗能力:' + this.supported).fontSize(18)
Text(this.windowState + ' ' + this.stopReason).fontSize(14)
Text(this.lastRect).fontSize(12)
Row({ space: 12 }) {
Button('暂停任务').onClick(() => runtime.pause())
Button('恢复任务').onClick(() => runtime.resume())
Button('取消任务').onClick(() => runtime.cancel())
}
Row({ space: 8 }) {
Button('紧凑尺寸').onClick(() => this.resize(true))
Button('完整尺寸').onClick(() => this.resize(false))
Button('切换模板').onClick(() => this.changeTemplate())
}
Row({ space: 8 }) {
Button('隐藏窗口').onClick(() => this.setVisible(false))
Button('显示窗口').onClick(() => this.setVisible(true))
}
Button('开始整理').onClick(() => runtime.start())
Button('显示闪控窗').enabled(!this.busy).onClick(() => this.openFloat())
Button('停止闪控窗').onClick(() => this.closeFloat())
}.width('100%').height('100%').padding(32).justifyContent(FlexAlign.Center)
}
}
module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"requestPermissions": [{"name":"ohos.permission.FLOAT_VIEW","reason":"$string:float_reason","usedScene":{"abilities":["EntryAbility"],"when":"inuse"}}],
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
}
}
main_pages.json
{"src":["pages/Index","pages/FloatViewPage"]}
string.json
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "文件整理"
},
{
"name": "float_reason",
"value": "在闪控窗中查看本地任务状态"
}
]
}
更多推荐




所有评论(0)