前言

用户在其他页面继续处理事情时,闪控窗可能挡住正在阅读的内容。拖动可以改变窗口位置,暂时收起可以让出更多空间,而关闭则结束这一次悬浮查看。这些动作针对的是显示入口,文件整理本身可能仍在进行。如果应用把窗口消失直接理解为任务取消,用户只是整理屏幕布局,就可能意外结束正在等待的工作。

窗口不可见之后,下一次操作也取决于它去了哪里。隐藏的窗口可能继续保留,侧边栏中的入口可以恢复原窗口,已经停止的窗口则需要结束旧实例后重新创建。因此,一个表示是否显示的布尔值无法说明全部后续行为。应用需要知道系统报告了哪种状态,再决定是否保留控制器,不能先丢弃引用再尝试解释消失原因。

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": "在闪控窗中查看本地任务状态"
    }
  ]
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐