鸿蒙UI开发——自定义UI绘制帧率

49 阅读4分钟

1、概 述

随着设备屏幕的不断演进,当前主流设备采用LTPO屏幕(可变刷新率屏幕),此类屏幕支持在多个档位之间切换屏幕帧率。

对于快速变化的内容,如射击游戏,交互动画等,显示帧率越高,画面越流畅,但是相对的功耗也会越高。

而低速变化的内容,如游戏大厅,时钟更新动画等,画面更新频率较低,使用相对低的显示帧率,用户也不会觉得卡顿,但是相对的功耗就比较低。

基于显示内容的可变帧率能力,在具备LTPO屏幕的设备上,可以达到性能体验和功耗间的平衡。

HarmonyOS支持可变帧率能力,我们通过使用可变帧率接口,进行相关业务开发,可以享受可变帧率特性带来的功耗收益。

可变帧率为应用开发中的动画组件、XComponent组件、UI绘制等提供一种基础帧率配置和能力。

通过设置有效的期望绘制帧率后,系统会收集设置的请求帧率,进行决策和分发,在渲染管线上进行分频,尽量能够满足调用方的期望帧率。

【在我们自己希望独立绘制渲染一些内容时(例如使用Canvas自定义一些动态效果),也可以考虑用这种方式】

2、配置自定义UI绘制帧率

如果我们需要以独立的帧率绘制更新操作UI界面时,可以通过DisplaySync来实现。

模块如下:

import { displaySync } from '@kit.ArkGraphics2D';

创建一个DisplaySync对象方法如下:

let backDisplaySync: displaySync.DisplaySync = displaySync.create();

DisplaySync对象定义如下:

class DisplaySync {
  // 设置期望的帧率范围。
  setExpectedFrameRateRange(rateRange: ExpectedFrameRateRange) : void
  // 订阅每一帧变化
  on(type'frame', callback: Callback<IntervalInfo>): void
  // 取消订阅每一帧的变化
  off(type'frame', callback?: Callback<IntervalInfo>): void
  // 开始每帧回调
  start(): void
  // 停止每帧回调
  stop(): void
}

// 设置帧率范围的入参结构如下
class ExpectedFrameRateRange {
  min: number,  // 期望的最小帧率。
  max: number,  // 期望的最大帧率。
  expected: number, // 期望的最优帧率。
}

此处以不同帧率改变文件组件字体大小为例,来模拟不同UI绘制帧率的效果。步骤如下:

👉🏻 step 1:导入模块并定义DisplaySync对象。

import { displaySync } from '@kit.ArkGraphics2D';

@Entry
@Component
struct Index {
  // 定义两个DisplaySync变量(slow是30帧,fast是60帧),未初始化
  private backDisplaySyncSlow: displaySync.DisplaySync | undefined = undefined;
  private backDisplaySyncFast: displaySync.DisplaySync | undefined = undefined;
}

👉🏻 step 2:定义两个文本组件。

@State drawFirstSize: number = 25;
@State drawSecondSize: number = 25;
@Builder doSomeRenderFirst() {
 Text('30')
   .fontSize(this.drawFirstSize)
}

@Builder doSomeRenderSecond() {
 Text('60')
   .fontSize(this.drawSecondSize)
}

👉🏻 step 3:通过DisplaySync实例设置帧率和注册订阅函数。

CreateDisplaySyncSlow() {
    let range : ExpectedFrameRateRange = { // 创建和配置帧率参数
      expected: 30, // 设置期望绘制帧率为30hz
      min: 0, // 配置帧率范围
      max: 120 // 配置帧率范围
    };

    let draw30 = (intervalInfo: displaySync.IntervalInfo) => { // 订阅回调函数,字体大小在25到150之间变化
      if (this.isBigger_30) {
        this.drawFirstSize += 1;
        if (this.drawFirstSize > 150) {
          this.isBigger_30 = false;
        }
      } else {
        this.drawFirstSize -= 1;
        if (this.drawFirstSize < 25) {
          this.isBigger_30 = true;
        }
      }
    };

    this.backDisplaySyncSlow = displaySync.create(); // 创建DisplaySync实例
    this.backDisplaySyncSlow.setExpectedFrameRateRange(range); // 设置帧率
    this.backDisplaySyncSlow.on("frame", draw30); // 订阅frame事件和注册订阅函数
}

👉🏻 step 4:开始每帧回调

Button('Start')
  .id('CustomDrawStart')
  .fontSize(14)
  .fontWeight(500)
  .margin({ bottom: 10, left: 5 })
  .fontColor(Color.White)
  .onClick((): void => {
      if (this.backDisplaySyncSlow == undefined) {
        this.CreateDisplaySyncSlow();
      }
      if (this.backDisplaySyncFast == undefined) {
        this.CreateDisplaySyncFast();
      }
      if (this.backDisplaySyncSlow) {
        this.backDisplaySyncSlow.start();
      }
      if (this.backDisplaySyncFast) {
        this.backDisplaySyncFast.start();
      }
    })
    .width('20%')
    .height(40)
    .shadow(ShadowStyle.OUTER_DEFAULT_LG)

👉🏻 step 5:结束每帧回调****

Button('Stop')
  .id('CustomDrawStop')
  .fontSize(14)
  .fontWeight(500)
  .margin({ bottom: 10, left: 5 })
  .fontColor(Color.White)
  .onClick((): void => {
    if (this.backDisplaySyncSlow) {
        this.backDisplaySyncSlow.stop();
    }
    if (this.backDisplaySyncFast) {
        this.backDisplaySyncFast.stop();
    }
  })
  .width('20%')
  .height(40)
  .shadow(ShadowStyle.OUTER_DEFAULT_LG)

3、一个完整实例

import { displaySync } from '@kit.ArkGraphics2D';

@Entry
@Component
struct Index {
  @State drawFirstSize: number = 25;
  @State drawSecondSize: number = 25;
  private backDisplaySyncSlow: displaySync.DisplaySync | undefined = undefined;
  private backDisplaySyncFast: displaySync.DisplaySync | undefined = undefined;
  private isBigger_30:boolean = true;
  private isBigger_60:boolean = true;

  @Builder doSomeRenderFirst() {
    Text('30')
      .fontSize(this.drawFirstSize)
  }

  @Builder doSomeRenderSecond() {
    Text('60')
      .fontSize(this.drawSecondSize)
  }

  CreateDisplaySyncSlow() {
    // 定义期望绘制帧率
    let range : ExpectedFrameRateRange = {
      expected: 30,
      min: 0,
      max: 120
    };

    let draw30 = (intervalInfo: displaySync.IntervalInfo) => {
      if (this.isBigger_30) {
        this.drawFirstSize += 1;
        if (this.drawFirstSize > 150) {
          this.isBigger_30 = false;
        }
      } else {
        this.drawFirstSize -= 1;
        if (this.drawFirstSize < 25) {
          this.isBigger_30 = true;
        }
      }
    };

    this.backDisplaySyncSlow = displaySync.create(); // 创建DisplaySync实例
    this.backDisplaySyncSlow.setExpectedFrameRateRange(range); // 设置帧率
    this.backDisplaySyncSlow.on("frame", draw30); // 订阅frame事件和注册订阅函数
  }

  CreateDisplaySyncFast() {
    // 定义期望绘制帧率
    let range : ExpectedFrameRateRange = {
      expected: 60,
      min: 0,
      max: 120
    };

    let draw60 = (intervalInfo: displaySync.IntervalInfo) => {
      if (this.isBigger_60) {
        this.drawSecondSize += 1;
        if (this.drawSecondSize > 150) {
          this.isBigger_60 = false;
        }
      } else {
        this.drawSecondSize -= 1;
        if (this.drawSecondSize < 25) {
          this.isBigger_60 = true;
        }
      }

    };

    this.backDisplaySyncFast= displaySync.create(); // 创建DisplaySync实例
    this.backDisplaySyncFast.setExpectedFrameRateRange(range); // 设置帧率
    this.backDisplaySyncFast.on("frame", draw60); // 订阅frame事件和注册订阅函数
  }

  aboutToDisappear() {
    if (this.backDisplaySyncSlow) {
      this.backDisplaySyncSlow.stop(); // DisplaySync失能关闭
      this.backDisplaySyncSlow = undefined; // 实例置空
    }
    if (this.backDisplaySyncFast) {
      this.backDisplaySyncFast.stop(); // DisplaySync失能关闭
      this.backDisplaySyncFast = undefined; // 实例置空
    }
  }

  build() {
    Column() {
      Row() {
        this.doSomeRenderFirst();
      }
      .height('40%')

      Row() {
        this.doSomeRenderSecond();
      }
      .height('40%')

      Row() {
        Button('Start')
          .id('CustomDrawStart')
          .fontSize(14)
          .fontWeight(500)
          .margin({ bottom: 10, left: 5 })
          .fontColor(Color.White)
          .onClick((): void => {
            if (this.backDisplaySyncSlow == undefined) {
              this.CreateDisplaySyncSlow();
            }
            if (this.backDisplaySyncFast == undefined) {
              this.CreateDisplaySyncFast();
            }
            if (this.backDisplaySyncSlow) {
              this.backDisplaySyncSlow.start(); // DisplaySync使能开启
            }
            if (this.backDisplaySyncFast) {
              this.backDisplaySyncFast.start(); // DisplaySync使能开启
            }
          })
          .width('20%')
          .height(40)
          .shadow(ShadowStyle.OUTER_DEFAULT_LG)

        Button('Stop')
          .id('CustomDrawStop')
          .fontSize(14)
          .fontWeight(500)
          .margin({ bottom: 10, left: 5 })
          .fontColor(Color.White)
          .onClick((): void => {
            if (this.backDisplaySyncSlow) {
              this.backDisplaySyncSlow.stop(); // DisplaySync失能关闭
            }
            if (this.backDisplaySyncFast) {
              this.backDisplaySyncFast.stop(); // DisplaySync失能关闭
            }
          })
          .width('20%')
          .height(40)
          .shadow(ShadowStyle.OUTER_DEFAULT_LG)
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .shadow(ShadowStyle.OUTER_DEFAULT_SM)
      .alignItems(VerticalAlign.Bottom)
      .layoutWeight(1)
    }
  }
}

效果如下:

image.png