目录

高效使用模糊

概述

在图像处理中,模糊效果(Blur Effect)是一种通过算法降低图像局部或整体的清晰度、减少细节或噪声的技术。模糊常用于突出主体、模拟景深、隐藏敏感信息或为后续处理(如边缘检测)做准备。

本文介绍的背景模糊属于动态模糊,根据效果区分为材质模糊和普通模糊。背景模糊在实际应用中应用场景广泛,例如:在包含多个卡片的页面中,应用背景模糊效果可突出层次感;在页面转场时,添加背景模糊能使过渡更加平滑。为进一步提升效果,系统通过使用缓存技术优化动态模糊的性能。本文旨在探讨背景模糊技术的高效应用方法,结合应用场景进行说明。

效果分类 接口与使用说明
材质模糊

.backgroundBlurStyle():通过枚举值的方式封装了不同的模糊半径、亮度、饱和度、蒙版颜色。

适用场景:通过设置枚举值来使用系统已经封装好的效果。

.backgroundEffect():可以自定义模糊半径、亮度、饱和度、蒙版颜色、取色方式以及灰阶参数。

适用场景:用户需要自定义效果的场景。

普通模糊

.backdropBlur():仅能设置模糊半径和灰阶参数。

适用场景:仅需设置模糊半径的场景,如:页面转场。

实现原理

[h2]系统渲染工作流程

RenderService (RS) :系统渲染服务进程,接收来自于其他系统服务进程(如桌面进程)及用户进程(如应用)的自渲染图层及ArkUI控件绘制指令,进行统一的组合以及渲染控制。其渲染动作会调用CPU/GPU等通用计算器件进行。

[h2]RS模糊缓存工作流程

RenderThread (RT) :渲染线程,RS进程划分为主线程和RT线程,RT线程负责接收各种渲染参数,渲染各种图像效果并上屏。

基本逻辑:根据需要创建或者刷新Drawable,同步模糊参数;做模糊先截图(生成截图的缓存),再做模糊(生成模糊的缓存),最多存在一份缓存(截图或者模糊结果)。

(1)主线程:创建/刷新Drawable;判断模糊是否重绘:模糊区域改变、模糊参数改变、是否与脏区相交;同步模糊参数。

(2)RT线程:如缓存有效则复用缓存,否则RT线程重做模糊,并重新缓存。

合理使用背景模糊的取色方式

[h2]场景描述

如果需要设置背景模糊的蒙版颜色,有两种取色方式可以选择,这里推荐使用ColorPicker取色方式,性能更优。

效果图

[h2]实现原理

如果要设置背景模糊的蒙版颜色,有如下两种取色方式:

(1)方式1:直接设置AdaptiveColor.AVERAGE取色。此方式需要RS在CPU计算阶段,先用GPU绘制一遍取色区域,然后再计算区域平均颜色值。优点:开发方式简单,适合轻负载应用使用。缺点:性能较低。

(2)方式2:创建ColorPicker取色。此方式先使用EffectKit接口创建ColorPicker进行取色,再赋值给模糊参数中的color属性。优点:由于绘制取色区域的过程在应用层进行,所以性能有所提升,适合静态模糊场景。

[h2]开发步骤

方式1:设置背景模糊的接口参数adaptiveColor为AdaptiveColor.AVERAGE

  1. 对背景图片应用backgroundEffect接口设置模糊效果;
  2. 设置adaptiveColor属性值为AdaptiveColor.AVERAGE。

示例代码

@Component
export struct AdaptiveColorMode {
  @Consume('navPathStack') navPathStack: NavPathStack;

  build() {
    NavDestination() {
      Column() {
        Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
          Text('Background Material Blur').fontColor($r('sys.color.comp_background_list_card'))
        }
        .height('50%')
        .width('70%')
        .backgroundEffect({
          radius: 20,
          saturation: 1.0,
          brightness: 1.0,
          adaptiveColor: AdaptiveColor.AVERAGE
        })
        .position({ x: '15%', y: '30%' })
      }
      .height('100%')
      .width('100%')
      .backgroundImage($r('app.media.test'))
      .backgroundImageSize(ImageSize.Cover)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }
    .hideTitleBar(true)
  }
}

方式2:先使用ColorPicker进行取色,再给模糊接口的color参数赋值

  1. 传入图片资源,创建pixMap;
  2. 根据pixMap创建ColorPicker;
  3. 调用ColorPicker的getAverageColor()方法获取颜色,转换成backgroundEffect接口参数color的格式,调用接口时替换成获取到的blurColor。

示例代码

import { image } from '@kit.ImageKit';
import { effectKit } from '@kit.ArkGraphics2D';

@Component
export struct ColorPickerMode {
  @Consume('navPathStack') navPathStack: NavPathStack;
  @State pixMap: image.PixelMap | null = null
  @State kitColor: effectKit.Color = {
    red: 255,
    green: 255,
    blue: 255,
    alpha: 255
  };
  @State blurColor: string = 'rgba(255,255,255,255)';

  async blurPix(resource: Resource) {
    try {
      const context: Context = this.getUIContext().getHostContext()!;
      const fileData: Uint8Array = await context.resourceManager.getMediaContent(resource.id);
      const buffer: ArrayBufferLike = fileData.buffer
      let imageSource: image.ImageSource = image.createImageSource(buffer as ArrayBuffer)
      this.pixMap = await imageSource.createPixelMap();
      this.kitColor = (await effectKit.createColorPicker(this.pixMap, [0, 0, 1, 1])).getAverageColor();
      this.blurColor = 'rgba(' + this.kitColor.red + ',' + this.kitColor.green + ',' + this.kitColor.blue + ',0)';
    } catch (err) {
    }
  }

  build() {
    NavDestination() {
      Column() {
        Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
          Text('Background Material Blur').fontColor($r('sys.color.comp_background_list_card'))
        }
        .height('50%')
        .width('70%')
        .backgroundEffect({
          radius: 20,
          saturation: 1.0,
          brightness: 1.0,
          color: this.blurColor
        })
        .position({ x: '15%', y: '30%' })
      }
      .height('100%')
      .width('100%')
      .backgroundImage($r('app.media.test'))
      .backgroundImageSize(ImageSize.Cover)
      .onAppear(() => {
        this.blurPix($r('app.media.test'));
      })
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }
    .hideTitleBar(true)
  }
}

性能对比

测试使用两种取色方式绘制背景模糊效果的单帧渲染耗时,最终使用DevEco Studio内置的Profiler中的帧率分析工具Frame抓取绘制背景模糊时的性能差异。

如上图所示,通过RenderFrame(执行GPU绘制)标签可以看出,使用ColorPicker取色的单帧平均耗时为5.650ms;而直接设置背景模糊的接口参数为AdaptiveColor.Average的单帧平均耗时为9.400ms。

在背景模糊场景正确使用混合模式

[h2]场景描述

如果需要在背景模糊场景使用混合模式,需要结合实际场景选择合适的混合模式,才能得到预期的效果。以下是同一场景下使用两种不同的方式实现的效果图:

[h2]实现原理

BlendMode(混合模式):这是一种图形处理技术,用于控制两个或多个图层(或图形元素)在叠加时如何混合颜色。它决定了上层像素与下层像素的交互方式,从而产生不同的视觉效果。

BlendMode的类型有两种:一种是FAST类型,另一种是OFFSCREEN类型。FAST类型使各个图层的效果按顺序进行混合,而OFFSCREEN类型使用离屏画布,组件内容先绘制到离屏画布上,然后再整体进行混合。

因此当场景中同时存在BlendMode和背景模糊时,需要注意BlendMode的类型。BlendMode设置为OFFSCREEN类型时使用离屏画布进行绘制,而模糊处理过程需要对背景进行截图,如果离屏画布未完成绘制,模糊流程已经进行了对背景的截图并绘制,那么绘制结果可能是非预期的。

两种类型的差异见下图:

[h2]开发步骤

如果想得到最底层是背景图片,其上是地图,在地图上叠加模糊的效果,应使用FAST方式。

预期效果图

设置blendMode为FAST类型使各个图层的效果按顺序进行混合,所以可以实现预期的效果。

示例代码

@Component
export struct FastMode {
  @Consume('navPathStack') navPathStack: NavPathStack;

  build() {
    NavDestination() {
      Stack() {
        Stack() {
          Image($r('app.media.map')).width('100%').height('100%').borderRadius(15)
          Flex({ direction: FlexDirection.ColumnReverse }) {
          }
          .height(80)
          .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THIN, { scale: 0.3 })
          .flexShrink(0)
          .width('100%')
          .padding(0)
          .borderRadius({
            topLeft: 0,
            topRight: 0,
            bottomLeft: 15,
            bottomRight: 15
          })
          .position({ x: 0, y: 220 })
        }.margin({ left: '16', right: '16' })
        .height(300)
        .borderRadius(15)
        .blendMode(BlendMode.SRC_OVER, BlendApplyType.FAST)
      }
      .width('100%')
      .height('100%')
      .backgroundImage($r('app.media.img'))
      .backgroundImageSize(ImageSize.Cover)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }
    .hideTitleBar(true)
  }
}

非预期效果图

如果使用OFFSCREEN方式,将得到非预期的效果:背景模糊对背景图片进行模糊,而后与地图的图层进行效果混合。

设置blendMode为OFFSCREEN类型会创建离屏画布,而模糊处理过程需要对背景进行截图,当离屏画布未完成绘制回到屏幕上时,模糊流程已经进行了背景的截图并绘制,所以混合后的效果非预期。

示例代码

@Component
export struct OffscreenMode {
  @Consume('navPathStack') navPathStack: NavPathStack;

  build() {
    NavDestination() {
      Stack() {
        Stack() {
          Image($r('app.media.map')).width('100%').height('100%').borderRadius(15)
          Flex({ direction: FlexDirection.ColumnReverse }) {
          }
          .height(80)
          .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THIN, { scale: 0.3 })
          .flexShrink(0)
          .width('100%')
          .padding(0)
          .borderRadius({
            topLeft: 0,
            topRight: 0,
            bottomLeft: 15,
            bottomRight: 15
          })
          .position({ x: 0, y: 220 })
        }
        .margin({ left: '16', right: '16' })
        .height(300)
        .borderRadius(15)
        .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)
      }
      .width('100%')
      .height('100%')
      .backgroundImage($r('app.media.img'))
      .backgroundImageSize(ImageSize.Cover)
      .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
    }
    .hideTitleBar(true)
  }
}

总结

本文针对系统的背景模糊特性,深度解析其技术原理与实现机制,并基于不同应用场景提出高效实施方案。在涉及取色功能的开发场景中,建议开发者通过性能开销和视觉效果的量化评估,选择最优实现方案;对于需要使用混合模式进行上下层图层混合的场景,开发者应当结合混合模式和背景模糊的工作原理,选择合适的混合类型。

示例代码