前言

这两年做界面设计,最绕不开的风格大概就是毛玻璃了——iOS 的控制中心、macOS 的侧边栏、各种 App 的底部导航,全是半透明加模糊的路子。第一次在 HarmonyOS 6 里想复刻这种质感时,我以为要写一堆 Canvas 滤镜,翻了一圈文档才发现,ArkUI 早就把“背景模糊”做成了组件属性,一行代码的事。

但“能做出来”和“做得好看”之间隔着很远的距离。这篇我就从毛玻璃的构成讲起,把半透明、模糊、层次这三件事拆开揉碎,再给出三张不同气质的毛玻璃卡片和一把能实时调节模糊半径的滑块。代码在文末,跑起来之后,把滑块来回拖一拖,你对毛玻璃的理解会比看任何教程都深。

顺带说明环境:工程在 DevEco Studio 6.1.1 Release(Build #6.1.1.300)+ HarmonyOS 6.1.1 Release SDK(API Version 24)下编译运行,模拟器 MateBook Pro 2in1,全程零权限零网络。

玻璃从哪来:毛玻璃的三层构成

先把“毛玻璃”这三个字拆开。它其实由三样东西叠在一起:

  • 半透明:玻璃本身不是纯色,是带透明度的颜色(rgba),让底下的内容透过来。
  • 模糊:透过来的内容不能是清晰的,要被打散成柔和的光晕,这就是“毛”的来源。
  • 层次:玻璃下面得有东西,而且越丰富越好——纯色背景上做毛玻璃,等于在墙上贴一张磨砂膜,什么都看不清。

理解了这三层,你就会明白一个常见误区:毛玻璃效果不好,往往不是模糊参数调得不对,而是背景太单调。没有可模糊的内容,再大的模糊半径也是巧妇难为无米之炊。

所以在动手写卡片之前,我先把“背景”这件事做好。

这篇的标题叫“毛玻璃卡片”,但我想先打一个预防针:不要一上来就找“毛玻璃怎么做”的现成代码,而是先问自己三个问题——玻璃下面是什么?玻璃本身多透明?玻璃要多糊?三个问题想清楚了,代码只是顺手的事。后面的章节就是按这个顺序展开的。

让背景先丰富起来

背景设计的原则:要有渐变,要有对比,要有“内容感”。我用了三样东西:

  • 一个深色线性渐变铺满全屏,给整体定调(深色背景下毛玻璃的质感最明显);
  • 三个不同颜色、不同大小、不同位置的彩色圆形光斑,模拟真实界面里的图片和色块;
  • 光斑用半透明(opacity)叠在渐变上,让层次更丰富。
// 深色渐变底色
Column()
  .width('100%')
  .height('100%')
  .linearGradient({
    angle: 135,
    colors: [['#0F2027', 0.0], ['#203A43', 0.5], ['#2C5364', 1.0]]
  })

// 三个彩色光斑(模糊的“素材”)
Circle({ width: 260, height: 260 })
  .fill('#FF6D00')
  .opacity(0.55)
  .position({ x: -40, y: 60 })

光斑怎么放也是有讲究的:一部分要“伸出屏幕外”(比如 x 为负值),让画面有延伸感;彼此颜色要有色相差异,这样模糊之后才能看到柔和的光晕过渡,而不是糊成一团灰。

布局上我用了 Stack 做层叠:渐变底和光斑是一层,毛玻璃卡片和滑块是另一层,两者用 Stack 叠在一起,卡片才能隔着半透明“看见”底下的光斑。这也是毛玻璃类界面的通用结构——底层放内容,上层放玻璃,两层之间没有遮挡关系,只有层次关系。如果你用普通 Column 顺序排布,卡片上下排列,背面就什么都没有,毛玻璃自然无从谈起。

半透明的艺术:rgba 的透明度讲究

半透明用的是 rgba 颜色,最后一个通道就是透明度。这里有两个实用经验:

透明度别拍脑袋定。白色卡片 rgba(255,255,255,0.35) 适合浅色毛玻璃——太透明会看不清文字,太实心又没有玻璃感;深色卡片 rgba(0,0,0,0.38) 适合浮层和导航,稳重不刺眼。0.3~0.45 是个不容易出错的区间。

动态色可以拼字符串。如果你想让卡片底色跟着状态变,可以把主题色存成十六进制字符串,透明度后缀直接拼上去:this.colors[tint] + '66',相当于 40% 左右的透明度。ArkTS 的 backgroundColor 认这种写法,一行就实现了“换色不换模糊”。

模糊的两种姿势:blur 与 backdropBlur

ArkUI 里有两个容易混淆的模糊属性,我特意对比过:

  • blur:模糊组件自己的内容。给一张图片加 blur,图片自身变糊。
  • backdropBlur:模糊组件背后的内容。组件自己保持(或半透明),后面的背景被柔化——这才是毛玻璃要的。

毛玻璃卡片一律用 backdropBlur。参数是模糊半径,单位 px,值越大糊得越狠。半径的取值直接决定质感:个位数是“磨砂贴膜”,十几二十几是标准的毛玻璃,超过四十,背景基本就是一整片柔光。

// 毛玻璃卡片:半透明底 + 背景模糊
Column()
  .backgroundColor('rgba(255,255,255,0.35)')  // 半透明
  .backdropBlur(24)                             // 背景模糊
  .borderRadius(20)

三张卡片,三种气质

为了把毛玻璃的用法讲透,我做了三张卡片,每张都是一种真实场景的缩影:

  • 基础毛玻璃:半透明白底,标准半径,像信息卡片、设置面板——高频、百搭。
  • 深色毛玻璃:深色半透明底 + 更大的模糊半径,像底部导航栏、弹窗浮层——沉稳、有安全感。
  • 动态色毛玻璃:底色跟着“换色”按钮切换,像标签页、胶囊按钮——俏皮、有活力。

三种卡片共用同一套模糊逻辑,差别只在颜色和透明度上。这正好印证了前面的观点:毛玻璃的灵魂是那套“半透明 + 模糊”的组合,颜色只是皮肤。

卡片间距和圆角也值得说一句:18 的间距让三张卡片各自独立、不粘连;20 的圆角让玻璃边缘柔和,符合“玻璃是圆润物件”的直觉。如果圆角太小,毛玻璃会显得像一块生硬的贴纸。这些数字没有公式,但多调几次,你的审美会自动记住它们。

一个滑块,摸清模糊半径的脾气

参数光靠文字描述是不够的,我加了一个 0 到 60 的滑块,直接绑定卡片的模糊半径:

@State radius: number = 24;

Slider({ value: this.radius, min: 0, max: 60, style: SliderStyle.OutSet })
  .width('100%')
  .onChange((value: number) => {
    this.radius = Math.round(value);
  })

三张卡片的 backdropBlur 都绑定 this.radius(深色那张故意加 10,保持差异化),滑块一动,三张卡片同步变化。把半径滑到 0,背后的光斑瞬间变得锋利清晰——你会直观看到“没有模糊”是什么样;再缓缓滑上去,光斑慢慢融化,毛玻璃的质感一点点浮现。这个动态过程,比任何静态截图都有说服力。

半径实时变化对性能也是考验,实测在模拟器上拖动全程流畅,说明 ArkUI 对背景模糊做了不错的底层优化,不必担心毛玻璃组件会拖垮帧率。

滑块本身也是 ArkUI 里很常用的输入组件,和 SliderStyle.OutSet 这个外观样式配合,轨道和外圈滑块都带默认的圆角质感,放在深色卡片上正好压得住。如果你想把滑块换成更沉浸的形式,还可以用 SliderStyle.InSide 让滑块嵌在轨道里,视觉上更极简——这篇保持 OutSet,方便你一眼看到轨道和取值范围。

毛玻璃的适配与性能

做毛玻璃还有一个绕不开的话题:适配和性能。几个实测结论写在这里,帮你少走弯路。

深色背景下效果最佳。同样的半透明参数,放在亮色背景上很容易显得脏,放在深色背景上才有那种“通透又沉稳”的高级感。这也是为什么系统级毛玻璃几乎都长在深色 UI 上。你的应用如果主背景是浅色,建议把卡片透明度降到 0.3 以下,并且给卡片加一层淡淡的描边(border),让玻璃的边缘能“看得见”。

模糊半径不是越大越好。半径超过 40 之后,背景细节基本损失殆尽,画面开始发灰;而且半径越大,GPU 计算量越大。日常 UI 用 15~30 是黄金区间,视觉和性能都舒服。

滚动容器里别堆太多毛玻璃。每张毛玻璃卡片在滚动时都要重新计算模糊,卡片一多会明显感觉到帧率下降。如果列表里有大量卡片需要这种质感,更稳的做法是把毛玻璃只用在固定层(导航栏、浮层),列表内容本身保持常规样式。

透明度后缀的写法。ArkTS 里 backgroundColor 支持十六进制 8 位色值(前 6 位颜色,后 2 位透明度),也支持 rgba()。这篇的动态色卡片用的就是前者:把主题色字符串 + '66' 拼起来,透明度稳定在 40% 左右,比算 rgba 的十进制数好读。两种写法官方都支持,选一种顺手的统一用就行。

这些适配经验不是理论推演,是这三张卡片调出来的体感。毛玻璃好看,但要用在合适的地方——这也是“质感”和“花哨”的分界线。

最后再补一个观察:毛玻璃在国内厂商的设计语言里几乎成了“高级感”的代名词,但也正因如此,它特别考验克制。一张卡片是点睛,满屏玻璃就是灾难。这篇的三张卡片各自承担一个用途说明,没有为炫技而堆数量,希望这个分寸感也能传递给你——组件是工具,最终服务的还是信息本身。

完整代码——直接运行

下面是 Index.ets 的完整代码,和模拟器上跑的是同一份。Empty Ability 工程,替换文件即可,不需要任何权限和资源:

@Entry
@Component
struct Index {
  @State radius: number = 24;
  @State tint: number = 0;
  private colors: string[] = ['#2B5CE6', '#7C4DFF', '#00BFA5', '#FF6D00'];

  build() {
    Stack({ alignContent: Alignment.TopStart }) {
      // ===== 底层:丰富背景(毛玻璃模糊的对象)=====
      Column()
        .width('100%')
        .height('100%')
        .linearGradient({
          angle: 135,
          colors: [['#0F2027', 0.0], ['#203A43', 0.5], ['#2C5364', 1.0]]
        })

      // 背景上的彩色光斑(被模糊后形成柔和光晕)
      Circle({ width: 260, height: 260 })
        .fill('#FF6D00')
        .opacity(0.55)
        .position({ x: -40, y: 60 })
      Circle({ width: 200, height: 200 })
        .fill('#2B5CE6')
        .opacity(0.6)
        .position({ x: 240, y: 320 })
      Circle({ width: 240, height: 240 })
        .fill('#00E5FF')
        .opacity(0.4)
        .position({ x: 60, y: 560 })

      // ===== 毛玻璃卡片区 =====
      Scroll() {
        Column({ space: 18 }) {
          Text('毛玻璃卡片')
            .fontSize(20)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.White)
            .margin({ top: 24 })

          Text('背景模糊 + 半透明,模糊半径实时可调')
            .fontSize(13)
            .fontColor('rgba(255,255,255,0.75)')

          // 卡片 1:基础毛玻璃
          Column({ space: 6 }) {
            Text('基础毛玻璃')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor('#1A1A1A')
            Text('半透明白底 + backdropBlur,后面的彩色光斑被柔化成一片光晕。')
              .fontSize(13)
              .fontColor('#444444')
              .lineHeight(20)
          }
          .width('100%')
          .padding(18)
          .backgroundColor('rgba(255,255,255,0.35)')
          .backdropBlur(this.radius)
          .borderRadius(20)
          .alignItems(HorizontalAlign.Start)

          // 卡片 2:深色系毛玻璃
          Column({ space: 6 }) {
            Text('深色毛玻璃')
              .fontSize(16)
              .fontWeight(FontWeight.Bold)
              .fontColor(Color.White)
            Text('深色半透明底 + 更大模糊半径,常用于底部导航栏、弹窗浮层。')
              .fontSize(13)
              .fontColor('rgba(255,255,255,0.85)')
              .lineHeight(20)
          }
          .width('100%')
          .padding(18)
          .backgroundColor('rgba(0,0,0,0.38)')
          .backdropBlur(this.radius + 10)
          .borderRadius(20)
          .alignItems(HorizontalAlign.Start)

          // 卡片 3:动态色毛玻璃(颜色可切换)
          Column({ space: 8 }) {
            Row() {
              Text('动态色毛玻璃')
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#1A1A1A')
              Blank()
              Button('换色')
                .height(28)
                .fontSize(12)
                .backgroundColor(this.colors[this.tint])
                .onClick(() => {
                  this.tint = (this.tint + 1) % this.colors.length;
                })
            }
            .width('100%')

            Text('卡片底色跟随按钮切换,半透明叠加模糊,用来做标签页、胶囊按钮都不违和。')
              .fontSize(13)
              .fontColor('#444444')
              .lineHeight(20)
          }
          .width('100%')
          .padding(18)
          .backgroundColor(this.colors[this.tint] + '66')
          .backdropBlur(this.radius)
          .borderRadius(20)
          .alignItems(HorizontalAlign.Start)

          // 模糊半径控制
          Column({ space: 8 }) {
            Row() {
              Text('模糊半径')
                .fontSize(13)
                .fontColor('rgba(255,255,255,0.85)')
              Blank()
              Text(`${this.radius} px`)
                .fontSize(13)
                .fontColor('#FFFFFF')
            }
            .width('100%')

            Slider({ value: this.radius, min: 0, max: 60, style: SliderStyle.OutSet })
              .width('100%')
              .onChange((value: number) => {
                this.radius = Math.round(value);
              })
          }
          .width('100%')
          .padding(18)
          .backgroundColor('rgba(255,255,255,0.2)')
          .borderRadius(20)
          .alignItems(HorizontalAlign.Start)

          Text('提示:把模糊半径滑到 0,光斑立刻变得锋利;滑到 60,背景几乎化成一团柔光。毛玻璃的“质感”就在这个区间里。')
            .fontSize(12)
            .fontColor('rgba(255,255,255,0.7)')
            .lineHeight(19)
            .padding({ bottom: 24 })
        }
        .width('100%')
        .padding({ left: 18, right: 18 })
      }
      .width('100%')
      .height('100%')
      .align(Alignment.Top)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0F2027')
  }
}

效果怎么说

模拟器上跑起来,画面是这么一幅图:深色渐变背景上漂浮着橙、蓝、青三团光斑,三张毛玻璃卡片依次排开——白玻璃通透清爽,黑玻璃沉稳内敛,彩色玻璃活泼跳脱。卡片上的文字清晰可读,背后被柔化的光斑若隐若现,那种“隔着一层玻璃看世界”的层次感一下就出来了。

把底部滑块从 0 缓缓拖到 60:先是光斑原形毕露,锐利得刺眼;随后边缘开始融化,光晕一点点晕开;到 40 以上,背景基本成了一大片柔光,卡片像是从光里长出来的。来回拖几遍,你会对“模糊半径”这个参数建立真正的肌肉记忆。

点一下动态色卡片的“换色”,蓝、紫、青、橙轮番上阵——颜色在变,玻璃的质感始终在线。这就是毛玻璃的魅力:半透明和模糊定了调,颜色随便玩。

如果你手头有真机,建议也装一份对比:模拟器的渲染和真机略有差异,真机上的毛玻璃过渡会更细腻。不过这篇的验证主体是模拟器,所有效果在 MateBook Pro 2in1 上都能完整体验,不必担心环境问题。

这篇从配色到布局、从模糊到性能都过了一遍,最后落到一句话:毛玻璃不是一种滤镜,是一种设计思路。先想清楚要表达什么层次,再决定玻璃怎么开。祝你在 HarmonyOS 6 里做出真正有质感的界面。

对了我用的工程模板是 Empty Ability,页面文件只有一个 Index.ets,想加第三张卡片就照着复制一段改改颜色和文案,很快。

哦对了,换色按钮那张卡片,透明度的 66 后缀就是十六进制的 0x66,约 40%,你也可以换成 99(60%)或 4D(30%)感受不同浓度。

补充一句运行提示:这个 demo 对设备没有特殊要求,模拟器真机都能跑,也不需要网络和权限,放心大胆点。

总结

这篇把毛玻璃拆成了三层:半透明、模糊、层次。代码上其实只用了两个属性——backgroundColor 给半透明,backdropBlur 给背景模糊——但真正的功夫在背景设计上:没有丰富的底层内容,毛玻璃就是一张磨砂纸。

回看整篇,最值得记住的不只是 backdropBlur 这个属性名,而是“先丰富背景,再谈模糊”的顺序。很多效果做出来寡淡,不是技术不行,是思路反了——先把底层铺满光与色,玻璃才有东西可映。这个思路放在任何视觉特效上都成立。

留给你的课后作业:把背景换成一张真实照片(比如风景图),卡片做成底部导航栏的样子;或者试试 blur 和 backdropBlur 同时用,做一层“玻璃上的雾”。毛玻璃这条路,越玩越有意思。

参考资料

  1. 背景模糊 backdropBlur(官方 API 参考)
  2. 图像模糊 blur(官方 API 参考)
  3. 渐变 linearGradient(官方 API 参考)
  4. Slider 滑块组件(官方 API 参考)
Logo

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

更多推荐