HarmonyOS ArkGraphics 2D场景化可变帧率实操——做一个智能帧率专注计时器

HarmonyOS ArkGraphics 2D场景化可变帧率实操------做一个智能帧率专注计时器

先看完整操作:把时间从5分钟拖到25分钟,再回到1分钟,随后开始、暂停、恢复并复位。紫色交互层只在拖动时运行,绿色核心层只在计时时运行。

这段GIF来自真机连续操作。拖动、倒计时、暂停和复位都在同一实验页中完成,不是用几张结果图拼接。

一个页面里的动画,不一定都值得长期运行在同一帧率。

专注计时器的背景光带只是氛围,30 FPS 已经能持续变化;中央倒计时和进度圆环是正在阅读的核心内容,运行时请求 60 FPS;时间滑块只有手指拖动的几秒需要高响应,因此只在拖动期间临时请求 90 FPS,松手后立即停止。

这次做出的页面不是帧率测试面板,而是一个可以选择写作、阅读或编码任务,可以设置 1~25 分钟,可以开始、暂停、继续和复位的专注计时器。

真机运行约 10 秒后,背景氛围收到 309 次回调,核心倒计时收到 618 次回调,页面直接显示"装饰回调减少 50%":

本次结果

页面状态 正在运行的内容 真机结果
等待设置 氛围层 30 FPS 观察约 29.9~30.0 FPS
拖动时间 氛围层 30 FPS + 交互层请求 90 FPS 交互观察约 59.9 FPS,松手后停止
专注运行 氛围层 30 FPS + 核心层 60 FPS 309 / 618 次回调,差值 50%
暂停计时 氛围层继续,核心层停止 4 秒内时间和核心回调不变
恢复计时 氛围层 + 核心层重新运行 剩余时间继续减少,核心回调继续增长
复位 只保留氛围层 核心和交互统计归零

这里没有把"回调减少 50%"写成"省电 50%"。本次只测量了应用收到的动画回调,没有测量整机功耗、温度或续航。

华为官方的 ArkGraphics 2D 简介 提到,同一窗口中的不同动画和自绘制内容可以使用不同帧率。这个专注计时器把能力落在了一个容易复用的产品结构里:非关键内容低频持续、核心内容按需运行、短时交互临时提高请求。

实验准备

本次环境如下:

项目 实际环境
设备 HUAWEI Mate 60 Pro
系统 HarmonyOS 7.0
SDK API 26
开发方式 ArkTS / ArkUI
相机与 AR 不需要
其他权限 不需要

页面使用三个 AnimatorResult。创建方式可以对照官方 帧动画开发指导

  • getUIContext().createAnimator() 创建动画;
  • setExpectedFrameRateRange() 为单个内容设置期望范围;
  • onFrame 接收动画进度并更新当前图层。

先把页面内容分成三层

帧率不是按照组件类型分配,而是按照内容在当前状态中的作用分配。

氛围层:30 FPS

背景层包含轨道光点、底部波形和渐变光团。它们负责让页面保持动态,但不承载倒计时信息,也不响应用户手指。

ArkTS/ets 复制代码
const AMBIENT_FPS: number = 30;

private ambientAnimator?: AnimatorResult;

private onAmbientFrame(progress: number): void {
  if (this.isReleased) {
    return;
  }

  this.ambientProgress = progress;
  const now: number = Date.now();

  if (this.isRunning) {
    this.ambientOverlapCallbacks += 1;
  }

  if (this.ambientRuntime.recordFrame(now)) {
    this.publishRuntime(
      'ambient',
      this.ambientRuntime,
      now
    );
  }
}

氛围层在页面打开后就开始运行,暂停倒计时时也不会消失。

核心层:60 FPS

中央圆环、剩余时间和完成百分比是计时器的核心。它们只在专注开始或恢复后运行:

ArkTS/ets 复制代码
const TIMER_FPS: number = 60;

private timerAnimator?: AnimatorResult;

private onTimerFrame(progress: number): void {
  if (this.isReleased || !this.isRunning) {
    return;
  }

  this.timerPulse = progress;
  const now: number = Date.now();

  this.remainingMs = Math.max(
    0,
    this.targetEndAt - now
  );

  if (this.timerRuntime.recordFrame(now)) {
    this.publishRuntime(
      'timer',
      this.timerRuntime,
      now
    );
  }
}

核心层停止后,不再通过动画回调更新剩余时间。

交互层:拖动时临时请求 90 FPS

交互层不做常驻动画。用户开始拖动 Slider 时才播放 Animator,松开手指就暂停:

ArkTS/ets 复制代码
const INTERACTION_FPS: number = 90;

private handleDurationChange(
  value: number,
  mode: SliderChangeMode
): void {
  if (this.hasStarted) {
    return;
  }

  const minutes: number = Math.round(value);
  this.selectedMinutes = minutes;
  this.totalDurationMs = minutes * MINUTE_MS;
  this.remainingMs = this.totalDurationMs;

  if (mode === SliderChangeMode.Begin ||
    mode === SliderChangeMode.Moving) {
    this.beginAdjustment();
    return;
  }

  this.endAdjustment();
}

beginAdjustment() 打开交互层:

ArkTS/ets 复制代码
private beginAdjustment(): void {
  if (this.isAdjusting ||
    !this.interactionAnimator) {
    return;
  }

  const now: number = Date.now();
  this.isAdjusting = true;
  this.interactionRuntime.begin(now);
  this.interactionAnimator.play();

  this.statusText = '正在调节专注时长';
  this.strategyText =
    '滑块反馈临时请求 90 FPS,松手后停止';
}

endAdjustment() 关闭交互层,并保留刚才的观察值和累计回调:

ArkTS/ets 复制代码
private endAdjustment(): void {
  if (!this.isAdjusting) {
    return;
  }

  const now: number = Date.now();
  this.interactionAnimator?.pause();

  if (this.interactionRuntime.end(now)) {
    this.publishRuntime(
      'interaction',
      this.interactionRuntime,
      now
    );
  }

  this.isAdjusting = false;
  this.interactionProgress = 0;
}

创建三个独立 Animator

三个图层都通过同一个辅助方法创建,但每次传入自己的目标 FPS:

ArkTS/ets 复制代码
private createLayerAnimator(
  targetFps: number,
  frameHandler: (progress: number) => void
): AnimatorResult {
  const animator: AnimatorResult =
    this.getUIContext().createAnimator({
      duration: 1200,
      easing: 'linear',
      delay: 0,
      fill: 'both',
      direction: 'alternate',
      iterations: -1,
      begin: 0,
      end: 1
    });

  animator.setExpectedFrameRateRange({
    min: targetFps,
    max: targetFps,
    expected: targetFps
  });

  animator.onFrame = frameHandler;
  return animator;
}

页面初始化时分别传入 30、60 和 90:

ArkTS/ets 复制代码
this.ambientAnimator =
  this.createLayerAnimator(
    AMBIENT_FPS,
    (progress: number): void => {
      this.onAmbientFrame(progress);
    }
  );

this.timerAnimator =
  this.createLayerAnimator(
    TIMER_FPS,
    (progress: number): void => {
      this.onTimerFrame(progress);
    }
  );

this.interactionAnimator =
  this.createLayerAnimator(
    INTERACTION_FPS,
    (progress: number): void => {
      this.onInteractionFrame(progress);
    }
  );

页面出现后只播放 ambientAnimator。另外两个实例已经创建,但还没有开始回调。

倒计时不能按回调次数递减

30、60、90 FPS 的回调次数不同,如果每次 onFrame 都固定减去 16 ms,不同帧率和调度波动会让计时速度发生变化。

本次开始专注时先保存真实结束时间:

ArkTS/ets 复制代码
private startFocus(): void {
  const now: number = Date.now();

  this.totalDurationMs =
    this.selectedMinutes * MINUTE_MS;
  this.remainingMs = this.totalDurationMs;
  this.targetEndAt = now + this.remainingMs;

  this.isRunning = true;
  this.hasStarted = true;

  this.ambientRuntime.reset();
  this.ambientRuntime.begin(now);
  this.timerRuntime.reset();
  this.timerRuntime.begin(now);

  this.timerAnimator?.play();
}

每次核心回调只做一件事:targetEndAt - Date.now()。回调少一次或短暂延迟,不会让总倒计时跟着少算或多算一帧。

用同一观察窗计算回调节奏

每个图层都有独立的运行对象。对象按约 800 ms 形成一个观察窗:

ArkTS/ets 复制代码
recordFrame(now: number): boolean {
  this.callbackCount += 1;

  if (this.windowStartedAt === 0) {
    this.windowStartedAt = now;
    this.windowFrameCount = 1;
    return false;
  }

  this.windowFrameCount += 1;
  const elapsed: number =
    now - this.windowStartedAt;

  if (elapsed < METRIC_WINDOW_MS ||
    this.windowFrameCount < 2) {
    return false;
  }

  const intervals: number =
    this.windowFrameCount - 1;

  this.observedFps =
    intervals * 1000 / elapsed;
  this.averageIntervalMs =
    elapsed / intervals;

  this.windowStartedAt = now;
  this.windowFrameCount = 1;
  return true;
}

观察值描述的是应用收到的 onFrame 节奏,不是外部仪器测得的面板物理刷新率。

真机操作一:拖动时临时启动交互层

页面初始为 5 分钟。真机先从 5 分钟慢拖到 25 分钟,再从 25 分钟拖回 1 分钟。

拖动过程中,圆环外侧出现紫色高响应光晕,中央明确显示"高响应调节 · 请求 90 FPS":

两次操作的日志:

text 复制代码
FOCUS_ADJUST_BEGIN target=90
selectedMinutes=5

FOCUS_ADJUST_END selectedMinutes=25
callbacks=342 observed=59.9 activeMs=5712

FOCUS_ADJUST_BEGIN target=90
selectedMinutes=25

FOCUS_ADJUST_END selectedMinutes=1
callbacks=735 observed=59.9 activeMs=12261

本机当前条件下,90 FPS 请求观察到约 59.9 FPS。松手后交互层停在 735 次,之后启动、暂停和恢复倒计时时都没有继续增长。

真机操作二:运行时按内容优先级分配帧率

时长设为 1 分钟,任务选择"编码",点击"开始专注"。

text 复制代码
FOCUS_START task=编码 minutes=1
totalMs=60000 ambientTarget=30 timerTarget=60

约 10 秒时,页面剩余 0:50,完成度 17%:

text 复制代码
氛围层:请求 30,观察 30.0,309 次回调
核心层:请求 60,观察 59.9,618 次回调
交互层:请求 90,观察 59.9,735 次回调

这 10 秒里,氛围层和核心层处于同一有效运行时间段,309 正好约为 618 的一半,所以页面显示"装饰回调减少 50%"。

真机操作三:暂停只停止核心计时

点击"暂停"后,页面剩余 0:49,核心回调为 695,交互回调为 735:

text 复制代码
FOCUS_PAUSE remainingMs=48406
timerCallbacks=695
ambientCallbacks=348
ambientOverlapCallbacks=348

等待约 4 秒后再次检查:

项目 暂停瞬间 4 秒后
剩余时间 0:49 0:49
核心回调 695 695
交互回调 735 735
氛围总回调 411 585
同时段回调差 50% 50%

暂停没有冻结整个页面。核心时间和进度停止,背景波形仍以 30 FPS 请求保持动态。

遇到的问题:暂停后回调差被算错

首版直接用氛围总回调除以核心总回调:

ArkTS/ets 复制代码
const reduction: number =
  (1 - ambientCallbacks / timerCallbacks) * 100;

运行中两层同时开始,这个公式显示 50%。暂停后核心停止,氛围继续,旧公式却从 50% 变成了 18%。

问题不是帧率发生变化,而是分子继续累计、分母已经停止,比较的时间段不一致。

修订后增加 ambientOverlapCallbacks,只有核心正在运行时才累计氛围回调:

ArkTS/ets 复制代码
if (this.isRunning) {
  this.ambientOverlapCallbacks += 1;
}

const reduction: number = Math.max(
  0,
  Math.min(
    100,
    (1 - this.ambientOverlapCallbacks /
      this.timerCallbacks) * 100
  )
);

最终复测中,暂停 4 秒前后都保持 50%,而氛围总回调仍从 411 增长到 585。页面既能表现背景仍在运行,也不会用不一致的时间段计算结果。

恢复:重新建立结束时间

暂停时已经保存了真实 remainingMs。恢复时不沿用旧结束时间,而是从当前时刻重新计算:

ArkTS/ets 复制代码
private resumeFocus(): void {
  const now: number = Date.now();
  this.targetEndAt = now + this.remainingMs;

  this.timerRuntime.begin(now);
  this.isRunning = true;
  this.timerAnimator?.play();
}

恢复后页面从 0:49 继续减少到 0:43,核心回调从 695 增长到 1075:

text 复制代码
FOCUS_RESUME remainingMs=48406
timerCallbacks=695

恢复后:
remaining=0:43
timerObserved=59.9
timerCallbacks=1075
callbackReduction=50%

复位:核心与交互清零,氛围继续

复位后,计时器回到 1:00 和 0%,核心、交互统计归零,背景仍继续运行:

text 复制代码
FOCUS_RESET selectedMinutes=1
ambientContinues=true

remaining=1:00
ambientObserved=29.9 callbacks=62
timerObserved=0.0 callbacks=0
interactionObserved=0.0 callbacks=0

这里没有为了复位整个功能而销毁氛围 Animator。页面仍然处于可见状态,背景层继续承担自己的视觉角色。

退出时释放三个 Animator

页面退出时解除所有回调并取消动画:

ArkTS/ets 复制代码
private detachAnimator(
  animator?: AnimatorResult
): void {
  if (!animator) {
    return;
  }

  animator.onFrame =
    (_progress: number): void => {};
  animator.onRepeat = (): void => {};
  animator.onFinish = (): void => {};
  animator.onCancel = (): void => {};
  animator.cancel();
}

最终退出日志:

text 复制代码
FOCUS_RELEASE action=EXIT
ambientCallbacks=97
timerCallbacks=0
interactionCallbacks=0
remainingMs=60000

RETURNED_HOME=true

构建结果

场景页的静态回归覆盖三组帧率范围、真实结束时间、Slider 拖动状态、同时间段回调差、暂停、恢复、复位和释放。

最终 API 26 clean 签名构建结果:

text 复制代码
CompileArkTS... Finished
PackageHap... Finished
SignHap... Finished
BUILD SUCCESSFUL in 19 s 953 ms

签名 HAP 覆盖安装成功,Ability 启动成功。

最终效果

这个案例把可变帧率落在了四个实际状态里:

  • 等待设置时,只让背景氛围以 30 FPS 请求运行;
  • 拖动刻度时,临时启动 90 FPS 交互反馈;
  • 专注运行时,背景 30 FPS 与核心 60 FPS 同时工作;
  • 暂停时,核心停止回调,背景继续保持动态。

读者最终得到的不只是三个 FPS 数字,而是一种可以直接放进播放器、运动记录、下载进度、仪表盘和长时间运行页面的分层方法:先判断内容在当前状态中的作用,再决定它是否需要持续高频更新。

相关推荐
坚果的博客1 小时前
CPF-KMP-CMP:在鸿蒙上用 Kotlin 写跨平台应用
华为·kotlin·harmonyos
ai小陈7 小时前
PyTorch DataLoader数据加载性能排查:GPU利用率低的实操指南
人工智能·pytorch·python·深度学习·ai·gpu算力
Hstar_chen11 小时前
开源一款 HarmonyOS 服务器监控客户端:星辰云巡 1.0.3
服务器·华为·harmonyos
2501_9197490318 小时前
华为鸿蒙免费视频播放器APP—小羊免费播放器
华为·harmonyos·鸿蒙
2501_9197490319 小时前
华为鸿蒙免费背书背课文背稿子APP—小羊背诵
华为·harmonyos·鸿蒙
OH_TPC19 小时前
HarmonyOS APP开发---"随手画"白板涂鸦App,需要用到这个库
harmonyos
2601_9620779820 小时前
机器学习及其Python实践
pytorch·python·机器学习·tensorflow·scikit-learn
lilian23321 小时前
HarmonyOS 7 新特性(二十六)|LazyLayoutAlgorithm 自定义懒布局
华为·harmonyos
clorinda21 小时前
从零理解 PyTorch:用神经网络完成 MNIST 手写数字识别
人工智能·pytorch·神经网络