前言

用户通过闪控窗查看进度时,可能还需要返回应用确认任务说明或执行完整操作。小窗口已经对应当前任务,详情按钮如果只打开应用总览,用户就要再次寻找刚才查看的内容。因此,返回路径需要把这项任务交给主页面,让用户在完整界面中继续查看同一轮处理。

恢复主窗口与定位任务具有不同职责。系统能够知道闪控窗属于哪个主窗口,却不知道应用把任务详情放在哪个页面,也不会替应用决定当前编号对应哪项工作。主窗口出现在前台以后,如果接收端没有处理任务信息,界面仍可能停在原来的总览。联动实现因此需要把窗口恢复和业务定位接起来,并分别确认两个动作的结果。

HarmonyOS 7 提供从闪控窗恢复所属主窗口的能力,也允许在恢复时传入自定义参数。这个能力要求用户交互与窗口状态满足条件,应用不能把任务定时回调当成任意弹出主窗口的入口。系统负责恢复窗口并传递参数,接收端负责识别任务、更新显示位置和读取现有状态。接口调用之后,工程还需要处理前后台切换中的时序与失败,不能把按钮可见直接当作窗口已经允许恢复。

任务定位还要避免覆盖新数据。用户点击详情时,任务可能仍在增长,参数如果同时保存一份百分比,主页面晚一点接收后就可能把旧值写回。因此,当前方案只传任务标识,让详情从运行时读取最新进度。主页面已经能够提供完整操作以后,闪控窗是否继续覆盖在详情上也需要选择,这又使前台显隐设置与恢复结果产生了联系。

当前应用只有一项本地整理任务,总览和详情在同一页面切换,没有额外建立路由栈。主窗口恢复以后,页面根据任务标识显示详情,并继续读取内存中的任务状态。这个范围不包含冷启动或进程重建;详情入口需要先满足窗口恢复条件,再把任务信息交给应用接收端。

一、用户点击以后才能恢复所属主窗口

用户点击详情按钮以后,应用需要通过当前闪控窗的控制器恢复主窗口。系统要求调用与用户交互对应,窗口也需要处于允许恢复的状态。因此,当前恢复动作由闪控页面按钮触发,任务定时器不会在后台自动执行恢复。

restoreMainWindow() 恢复闪控窗所属的主窗口。主窗口已经在前台时,调用会抬升它的层级;主窗口在后台时,调用需要满足窗口状态和用户交互条件。这个接口不能作为任意后台时刻主动弹出应用的通用入口。

任务完成提醒与恢复主窗口需要分别设计。任务达到百分之百只说明业务结束,没有表示用户希望立即切换正在使用的应用,因此完成回调不能直接代替用户点击。产品需要完成提醒时,可以另行选择提示方式,恢复所属主窗口仍要遵守接口要求的交互条件。

restoreMainWindow() 要求闪控窗处于 STARTED,并且用户已经点击闪控窗。主窗口处于 PAUSED 生命周期或最近任务等受限状态时,也可能恢复失败。开发者把调用放在闪控页面按钮事件里,能够把动作与用户意图对应起来,但仍然需要处理状态变化期间的失败。

          Button('查看任务详情').onClick(() => runtime.restoreAction?.())

按钮通过任务对象保存的动作回调调用主页面中的恢复方法。回调只持有当前操作入口,控制器继续由主页面管理。窗口页面没有再创建控制器,也没有通过共享存储传递系统对象,因此业务数据和窗口操作仍然能够分别检查。

主页面出现时设置这个回调,页面销毁时清除引用。这样可以避免窗口页面长期持有已经销毁的页面动作。当前应用只有一个主页面实例,这个实现能够说明绑定关系;多个主窗口或复杂页面重建时,还需要进一步管理动作所属实例。

恢复前,应用确认当前仍然有控制器。接口调用放在异常处理范围内,失败只记录错误,不修改进度,也不把任务标记为取消。用户可以在窗口恢复可操作状态以后再次点击,而不会因为一次窗口错误丢失业务状态。

窗口恢复入口属于用户主动操作,所以按钮需要表达明确目的。当前按钮显示查看任务详情,用户可以预期应用回到当前任务;如果按钮只写打开,用户无法判断会进入总览还是任务内容。这个文案选择来自当前导航需求,系统接口只负责恢复所属主窗口,不负责解释应用准备展示哪个业务页面。

主窗口已经在前台时,恢复调用也不能被简单替换成开始任务。用户可能只是希望把被其他窗口遮住的应用带到前面,原任务没有因此变成新任务。当前恢复方法不调用运行时的开始逻辑,主页面随后接收标识并更新显示。窗口动作与业务动作分别保留以后,开发者就可以用同一个详情入口处理前台和后台两种查看场景,而不会改变进度。

二、主窗口接收任务标识,再决定显示详情

主窗口可以恢复以后,应用还需要接收用户要查看的任务信息。恢复参数提供定位线索,任务状态仍保存在运行时对象中,接收端根据标识决定显示内容。两项职责分开后,恢复动作不需要携带进度快照,也就不会在定位时把过去的百分比重新写入。

恢复参数用 taskId 表达用户要查看的任务,接收端根据相同键名识别它。当前只有一项任务,发送值固定为 local-001。调用只传定位信息,原进度仍然保存在运行时中,主页面不需要从参数复制百分比。

      const parameters: Record<string, Object> = {};
      parameters['taskId'] = 'local-001';
      await this.controller.restoreMainWindow(parameters);

恢复方法先声明 Record<string, Object> 参数对象,再写入 taskId。对象类型对应接口参数,键名和值由应用约定。接收端因此需要按相同约定读取与检查,系统传递参数不会自动替应用校验任务编号。

EntryAbility.onNewWant() 接收这组参数。当前入口只接受 local-001,匹配以后把详情显示标记写入 AppStorage,主页面通过 @StorageLink 更新界面。未知参数不会直接打开不存在的任务,也不会触发新的整理流程。

    if (want.parameters?.taskId === 'local-001') {
      AppStorage.setOrCreate<boolean>('openTaskDetail', true);
      console.info('FloatTask06 onNewWant=local-001');
    }

我让接收端只修改详情显示标记,没有重新调用任务开始方法,因为用户要查看的是已经运行的对象。重新开始会改变原来的进度和完成时间,超出这次查看操作的含义。已有工程如果在生命周期回调中执行初始化,还需要确认恢复参数不会进入重置任务的分支,再观察页面定位结果。

AppStorage 保存主页面的详情显示标记,任务进度则继续保存在已有 LocalStorage 中。用户进入详情时,应用只改变 openTaskDetail,不会将一份进度从一个存储搬到另一个存储。两个对象分别保存页面显示选择与任务结果,避免导航变化覆盖业务数据。

当前详情直接在同一个页面切换显示,因此没有增加额外页面文件或路由栈。返回任务总览按钮只清除详情标记,不销毁任务对象。需要多层导航的实际应用可以将任务标识交给自己的导航逻辑,但不应在每次恢复时无条件压入重复详情。

参数接收端只处理自己认识的任务标识,也避免把外部输入直接当作界面状态。当前只匹配固定值,其他值不会创建新任务,也不会覆盖已有进度。产品以后采用真实任务编号时,可以先查询该编号是否存在、用户是否仍然可以查看,再决定显示详情或者返回总览。这个校验发生在业务入口,管理窗口的接口无需承担任务数据检查。

当前详情显示标记初始为关闭,收到匹配标识后才打开。普通应用启动仍然显示任务总览,系统恢复参数不会被当成每次启动都必须执行的默认动作。这个区别让用户主动打开应用与从任务窗口进入详情具有各自明确的结果。需要支持冷启动参数的产品,还应在对应创建生命周期中处理输入,当前 onNewWant() 路径没有替代全部启动入口。

多任务应用接收编号时,还需要确认参数类型和任务是否存在。一个字符串可以符合参数类型,却未必对应有效任务,因此接收端需要完成查询后再显示详情。当前固定值比较直接拒绝不匹配输入,开发者扩展任务范围时不能省略存在性检查。

重复点击详情时,标记仍然保持开启,页面不会不断增加相同任务的副本。这个结果来自同页状态切换的实现方式,不能直接推广到使用 push 操作的导航页面。已有路由的应用需要确认详情是否已经存在,再决定更新参数还是进入新页面,否则用户返回时可能需要连续退出多份相同详情。

三、前台显隐与恢复时序一起决定联动结果

主页面已经能够显示详情以后,闪控窗继续覆盖在它上面,可能遮住完整操作区。应用可以提供前台隐藏设置,让用户在主窗口查看任务,离开主应用后再通过闪控窗查看。这个选择仍然保留控制器,窗口停止则是另一条独立操作。

用户启用应用内隐藏后,应用调用 setFloatViewVisibilityInApp(false),闪控窗在主应用位于前台时隐藏,主应用退到后台时仍可提供查看入口。用户关闭这项设置时,应用传入 true 恢复前台可见。用户再从闪控窗进入详情,主窗口恢复到前台,闪控窗随后按该设置隐藏。整个过程复用原控制器和任务存储,不需要重新建立窗口或初始化任务。

这个设置不等于停止。控制器仍然存在,任务也没有被取消,所以返回桌面时不需要再创建窗口。应用如果把隐藏回调当作停止并释放引用,后续会失去对仍然存在窗口的管理,甚至产生重复启动请求。

窗口属性与任务参数分别说明两个对象。getWindowProperties() 可以读取窗口矩形、显示屏和缩放,onNewWant() 则接收用户要查看的任务标识。开发者遇到主窗口已经出现但详情没有显示时,需要检查参数接收和页面标记,修改窗口大小不会修复任务定位。

用户选择前台隐藏后,窗口仍然是可用的查看入口;用户停止窗口后,这一轮窗口已经结束。应用需要按这两个动作分别管理控制器,不能在所有不可见状态下都清空引用。否则主应用退到后台时,仍然存在的窗口可能失去管理对象。

详情恢复后窗口隐藏,可以减少同一任务信息在屏幕上的重复覆盖,但这个选择依赖主页面已经能够显示完整任务。应用若恢复到不含任务进度的首页,直接隐藏窗口可能使用户失去查看位置。因此业务定位需要与显隐策略一起确认:主窗口回到前台只是系统层面的结果,用户能否看到目标任务还取决于应用处理参数之后的页面变化。

当前主页面一直观察原有任务存储,恢复时不需要向窗口询问当前百分比。窗口只是同一数据的另一个展示位置,可能处于隐藏或者暂存状态,不能把它当成业务数据的唯一保管者。参数因此只携带标识,当前进度由运行时提供;页面定位与数据读取连接起来以后,窗口是否仍然显示不会影响详情取得结果。

窗口从隐藏恢复显示时,内容已经出现并不保证系统已经报告 STARTED。恢复主窗口的调用如果发生得过早,接口可能返回 1300031。当前方法捕获这个错误并保留任务状态,用户在窗口可以操作后再次点击,原任务不会因为失败而归零。

开发者遇到这种恢复失败时,可以先检查按钮触发时的窗口状态,再检查错误码。窗口条件已经满足以后,主应用仍未显示目标详情,排查才继续进入 taskId、onNewWant() 和页面显示标记。这个顺序分别定位系统恢复与应用导航,避免把状态时机错误修改成任务参数问题。

当前方法捕获错误并保留日志,没有自动循环重试。恢复主窗口具有用户交互前提,后台不断重试可能偏离用户操作,也会掩盖状态问题。实际产品可以在窗口尚未就绪时禁用详情按钮,收到 STARTED 后再允许点击,并向用户提供明确反馈。

1300032 涉及用户点击、闪控窗前台状态以及主窗口生命周期等受限条件,应用不能把所有恢复失败都归因于编号错误。接口返回 1300031 时,开发者需要检查窗口是否已经启动并允许恢复;返回 1300032 时,则需要继续核对交互与主窗口状态。当前实现捕获错误,没有逐项验证所有系统限制。

任务对象的生命周期也决定恢复范围。当前进度位于内存,主窗口退到后台时没有重新创建对象,所以回到详情能够接着显示。进程终止以后,应用没有从磁盘恢复任务的能力,不能把一次前后台切换描述成进程重建恢复。

如果主窗口恢复成功,但 onNewWant() 没有匹配到任务,页面就不会切换到详情。参数已经匹配而页面仍显示总览时,开发者需要检查 openTaskDetail 的写入与 @StorageLink。这些错误都不会改变原任务进度,应用可以先修复定位关系,无需重新开始整理。

重复恢复也不应该覆盖任务数据。用户第一次点击时任务可能在百分之二十,第二次点击时已经继续增长,接收端始终读取存储现值即可。如果把第一次点击附带的进度保存为恢复状态,后一次返回就可能回退。当前没有在参数中传递百分比,减少了过期数值进入任务状态的机会,但实际持久化恢复仍然需要根据真实任务记录设计。

恢复动作使用可选回调,因此主页面解除动作绑定后,窗口按钮不会继续调用已经失效的主页面方法。这个保护只覆盖当前绑定方式,不能保证任意页面重建都能自动恢复动作。当前主页面出现时重新绑定,开发者增加其他主页面实例时,需要确认最后一次绑定属于哪个窗口,避免任务详情恢复到错误的宿主。

四、详情与总览切换继续使用原任务

用户进入详情以后,主页面读取原任务的状态和百分比。任务在用户离开主页面期间产生的进度已经保存在运行时中,所以详情可以直接显示最新结果。用户再返回总览时,页面也只改变显示位置,当前处理无需重启。

返回任务总览按钮只清除详情显示标记,没有清理计时器或将百分比归零。当前详情与总览属于同一页面,显示切换不会销毁任务对象。已有导航工程如果在页面退出时释放业务对象,开发者就需要重新确定任务寿命,避免一次返回操作停止仍在执行的工作。

主页面销毁时还会解除 restoreAction,窗口按钮随后不会继续调用旧页面方法。主页面重新出现时会重新绑定动作,但多个主窗口需要额外确认回调属于哪个宿主。当前单主页面的绑定不能直接保证多窗口定位正确。

总结

当前应用已经能够从闪控窗恢复所属主窗口,并通过任务标识显示同一任务详情。应用调用接口恢复主窗口,接收端确认目标对象,进度继续来自原任务存储,因此一次查看不会重新开始整理。参数没有携带旧百分比,也避免了定位过程覆盖后续任务结果。

单任务应用可以用固定标识定位详情,多个任务则需要查询用户实际选中的对象。应用增加第二项工作时,需要分别管理正在执行的任务与窗口正在展示的任务,再把选中的标识交给详情入口。无论任务数量怎样变化,恢复参数都不应该覆盖任务已经产生的新进度。

本专栏主要通过 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%')
          Button('查看任务详情').onClick(() => runtime.restoreAction?.())
          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 {
  restoreAction: (() => Promise<void>) | undefined = undefined;
  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('FloatTask06 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('FloatTask06 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('FloatTask06 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;
  @StorageLink('openTaskDetail') showDetail: boolean = false;
  @State stopReason: string = '';
  @State lastRect: string = '';
  private template: floatView.FloatViewTemplateType = floatView.FloatViewTemplateType.ROUNDED_RECTANGLE;
  private controller: floatView.FloatViewController | undefined = undefined;


  aboutToAppear(): void {
    runtime.restoreAction = () => this.restoreTask();
    this.supported = canIUse('SystemCapability.Window.SessionManager') && floatView.isFloatViewEnabled();
    console.info('FloatTask06 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('FloatTask06 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('FloatTask06 rect=' + JSON.stringify(info));
        this.lastRect = info.reason + ' ' + info.windowRect.left + ',' + info.windowRect.top;
        this.readProperties();
      });
      controller.onLimitsChange((limits: floatView.FloatViewLimits) => {
        console.info('FloatTask06 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('FloatTask06 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('FloatTask06 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('FloatTask06 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('FloatTask06 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('FloatTask06 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('FloatTask06 request=' + JSON.stringify(size));
      await this.controller.setWindowSize(size);
    } catch (error) { console.error('FloatTask06 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('FloatTask06 template=' + next + ' requested=' + JSON.stringify(limits.maxSize));
    } catch (error) { console.error('FloatTask06 templateError=' + (error as BusinessError).code); }
  }

  async restoreTask(): Promise<void> {
    if (!this.controller) { return; }
    try {
      const parameters: Record<string, Object> = {};
      parameters['taskId'] = 'local-001';
      await this.controller.restoreMainWindow(parameters);
      console.info('FloatTask06 restore=success');
    } catch (error) { console.error('FloatTask06 restore=' + (error as BusinessError).code); }
  }
  async setVisible(visible: boolean): Promise<void> {
    try { await this.controller?.setFloatViewVisibilityInApp(visible); }
    catch (error) { console.error('FloatTask06 visibility=' + (error as BusinessError).code); }
  }
  async closeFloat(): Promise<void> {
    if (!this.controller || this.busy) { return; }
    try { await this.controller.stop(); }
    catch (error) {
      console.error('FloatTask06 stop=' + (error as BusinessError).message);
    }
  }

  aboutToDisappear(): void {
    runtime.restoreAction = undefined;
    runtime.dispose();
  }

  build() {
    Column({ space: 4 }) {
      Text(this.showDetail ? '任务详情 · local-001' : '文件整理').fontSize(26).fontWeight(FontWeight.Bold)
      if (this.showDetail) {
        Text('本地文件整理 · 100 个模拟步骤').fontSize(16)
        Button('返回任务总览').onClick(() => { this.showDetail = false; })
      }
      Text('阶段 06 · 当前任务详情').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、测试、元服务和应用上架分发等。

更多推荐