鸿蒙原生 ArkTS 阴影效果深度解析——.shadow() 的深度与方向控制实战


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

在移动端和桌面端的 UI 设计中,阴影(Shadow)是一个极其重要的视觉元素。它不仅能让界面元素产生"层级感"和"立体感",还能引导用户的视觉焦点,提升整体的交互体验。从 Material Design 的 elevation(海拔)系统到苹果的 Human Interface Guidelines 中的视觉层次,阴影始终是界面设计中不可或缺的组成部分。

HarmonyOS NEXT 作为华为自主研发的全场景操作系统,其原生的 ArkUI 声明式框架提供了一套功能完善且性能优异的阴影渲染机制。在 ArkTS 中,开发者通过 .shadow() 这个链式调用的 API,可以轻松地为任意组件添加阴影效果,并且能够精确控制阴影的模糊半径、偏移方向、颜色和透明度。

本文将以一个完整的实战项目为例,深入剖析鸿蒙原生 ArkTS 中 .shadow() API 的使用方法、参数含义、最佳实践以及背后的视觉设计原理。我们将从零开始构建一个交互式的阴影演示页面,通过可视化的方式直观地理解每一个参数的作用。


二、HarmonyOS NEXT 与 ArkUI 框架简介

2.1 HarmonyOS NEXT 的意义

HarmonyOS NEXT(鸿蒙星河版)是华为完全剥离 Android 代码、基于 OpenHarmony 开发的纯自主研发操作系统。从 API 24 开始,HarmonyOS NEXT 不再兼容 Android 应用,全面拥抱鸿蒙原生生态。这意味着所有应用都必须使用鸿蒙的原生开发框架——ArkUI 进行开发。

2.2 ArkUI 框架的核心特性

ArkUI 是一套声明式 UI 开发框架,具有以下核心特性:

  • 声明式语法:使用 ArkTS 语言(基于 TypeScript 扩展),通过 @Component@Builder 装饰器声明 UI 结构,代码即 UI。
  • 链式调用:组件的样式和属性通过链式方法调用进行设置,如 .width().height().shadow() 等。
  • 状态驱动:使用 @State@Prop@Link 等装饰器管理组件状态,状态变化自动触发 UI 更新。
  • 跨平台能力:一套代码可编译运行在手机、平板、手表、车机、智慧屏等多种设备上。
  • 高性能渲染:底层使用自研的渲染引擎,提供 60fps 甚至 120fps 的流畅动画效果。

2.3 从传统 CSS 阴影到 ArkUI 阴影

对于有 Web 开发经验的读者来说,.shadow() API 的用法与 CSS 中的 box-shadow 属性非常相似。以下是两者的对比:

参数维度 CSS box-shadow ArkUI .shadow()
水平偏移 offset-x offsetX
垂直偏移 offset-y offsetY
模糊半径 blur-radius radius
扩散半径 spread-radius 无(通过 fill 控制)
颜色 color color
内置预设 ShadowStyle 枚举

这种相似性降低了 Web 开发者的学习曲线,同时 ArkUI 的 .shadow() API 更加简洁,参数命名更加直观。


三、.shadow() API 详解

3.1 API 签名与参数说明

在 ArkTS 中,.shadow() 方法定义在 CommonAttribute 上,意味着几乎所有 UI 组件都可以调用该方法。它的函数签名如下:

// 方式一:通过 ShadowOptions 对象精确控制
.shadow(value: ShadowOptions): T;

// 方式二:通过 ShadowStyle 枚举使用内置预设
.shadow(value: ShadowStyle): T;
ShadowOptions 接口详解
interface ShadowOptions {
  radius: number;         // 模糊半径,单位 vp(虚拟像素),必须大于等于 0
  color?: Color | string | Resource;  // 阴影颜色,推荐使用 rgba 格式带透明度
  offsetX?: number;       // 水平偏移量,单位 vp。正值向右,负值向左
  offsetY?: number;       // 垂直偏移量,单位 vp。正值向下,负值向上
  fill?: boolean;         // 是否填充阴影内部区域,默认为 true
}
ShadowStyle 枚举值
enum ShadowStyle {
  OUTER_DEFAULT_XS,  // 超小型外阴影
  OUTER_DEFAULT_SM,  // 小型外阴影
  OUTER_DEFAULT_MD,  // 中型外阴影
  OUTER_DEFAULT_LG,  // 大型外阴影
}

3.2 核心参数深度解读

radius —— 模糊半径(阴影深度的灵魂)

radius 是阴影效果中最关键的参数,它决定了阴影的模糊程度扩散范围

  • radius = 0:阴影完全清晰,没有任何模糊,看起来像一个硬边缘的"复制层"。在实际 UI 中很少使用。
  • radius = 4 ~ 8:轻微模糊,适合用于卡片、按钮等需要轻微层级感的场景。类似 Material Design 中 elevation = 2dp ~ 4dp 的效果。
  • radius = 15 ~ 25:中等模糊,阴影开始呈现明显的扩散感,适合用于对话框、弹出菜单等需要与背景明显区分的元素。
  • radius = 30 ~ 60:高度模糊,阴影几乎变成一个柔和的"光晕"效果,适合用于强调视觉焦点或创造氛围感。

从视觉心理学的角度来看,模糊半径模拟的是真实世界中"光源距离"的感知。当一个物体离承载它的表面越高(即 elevation 越大),其投射的阴影就越模糊、越扩散。这与 Material Design 中的高度隐喻完全一致。

offsetX / offsetY —— 偏移方向(光源位置的暗示)

offsetXoffsetY 共同决定了阴影的投射方向,它们在视觉上暗示了光源的位置

  • offsetX > 0, offsetY > 0:阴影向右下方偏移 → 光源位于左上方。这是最自然、最常用的方向,模拟了日常环境中顶灯从左上角照射的效果。
  • offsetX < 0, offsetY > 0:阴影向左下方偏移 → 光源位于右上方。
  • offsetX > 0, offsetY < 0:阴影向右上方偏移 → 光源位于左下方。这种"底部光照"效果较少见,可用于营造特殊的视觉氛围,如舞台灯光。
  • offsetX = 0, offsetY > 0:阴影仅向下偏移 → 光源位于正上方。这是 Material Design 推荐的默认方向,模拟了环境中环境光从正上方照射的效果。

在同一个应用或页面中,建议保持阴影方向的一致性,避免给用户造成"光源位置混乱"的不协调感。

color —— 阴影颜色的选择策略

阴影颜色的选择直接影响最终的真实感。推荐的策略包括:

  1. 使用带透明度的黑色rgba(0, 0, 0, 0.2) ~ rgba(0, 0, 0, 0.4) 是最安全、最通用的选择。黑色叠加任何背景色都会产生自然的阴影效果。
  2. 根据背景色调整透明度:浅色背景上透明度可以稍高(如 0.3),深色背景上透明度需降低(如 0.15),否则阴影会过于突兀。
  3. 色彩阴影:在某些场景下,可以使用与主题色同色系但更暗的颜色作为阴影色。例如,在橙色卡片下使用 rgba(200, 80, 0, 0.3) 会产生"彩色阴影"效果,视觉上更加协调。
  4. 透明度与 radius 联动:一般来说,radius 越大,颜色透明度应越高(alpha 值越小),因为大半径的阴影扩散范围大,如果颜色太深会显得"脏"。

四、实战项目:构建阴影效果演示页面

接下来,我们将完整地实现一个阴影效果演示应用。这个应用将包含五个展示部分,分别从不同维度展示 .shadow() API 的能力。

4.1 项目结构

entry/src/main/ets/pages/
  ├── Index.ets        // 主页面(我们的阴影演示页面)
  └── ...              // 其他页面

4.2 完整的 Index.ets 代码

下面是完整的页面代码,包含详细的注释说明:

/**
 * 鸿蒙原生 ArkTS 布局方式 —— 阴影效果:shadow 的深度与方向控制
 * ================================================================
 * 核心技术点:
 *   .shadow(ShadowOptions | ShadowStyle)
 *     - radius    : 模糊半径,值越大阴影越模糊扩散(深度感越强)
 *     - offsetX   : 水平偏移(正 = 向右,负 = 向左)
 *     - offsetY   : 垂直偏移(正 = 向下,负 = 向上)
 *     - color     : 阴影颜色,支持透明度(ARGB)
 *     - fill      : 是否在阴影区域内填充(默认 true)
 *
 * 本页面通过多组卡片对比,直观展示:
 *   1) 不同模糊半径(深度)的效果差异
 *   2) 不同偏移方向(右 / 下 / 左 / 上)的效果差异
 *   3) 组合自定义阴影
 */

// 注意:ShadowOptions 和 ShadowStyle 是 ArkUI 框架内置类型,
// 在使用 .shadow() API 时自动可用,无需额外 import。

@Entry
@Component
struct Index {
  // ---------- 状态变量(用于展示当前选中的阴影参数) ----------
  @State currentRadius: number = 20;       // 当前模糊半径
  @State currentOffsetX: number = 10;      // 当前水平偏移
  @State currentOffsetY: number = 10;      // 当前垂直偏移
  @State selectedPreset: string = '默认';   // 当前选中的预设名称

  build() {
    // ========== 最外层:可滚动容器 ==========
    Scroll() {
      Column({ space: 24 }) {
        // =========================================================
        // 页面标题
        // =========================================================
        Text('Shadow 阴影效果 · 深度与方向控制')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Black)
          .width('100%')
          .textAlign(TextAlign.Center)
          .margin({ top: 20, bottom: 8 })

        Text('模糊半径越大 → 阴影越模糊扩散,视觉"深度感"越强')
          .fontSize(14)
          .fontColor('#666666')
          .width('100%')
          .textAlign(TextAlign.Center)
          .margin({ bottom: 12 })

        // =========================================================
        // 第一部分:阴影深度对比(固定偏移,变化模糊半径)
        // =========================================================
        Text('一、阴影深度(radius)对比 —— 偏移固定 (10, 10)')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%')
          .padding({ left: 12 })

        Row({ space: 16 }) {
          // --- 深度 1:轻阴影(radius = 4) ---
          this.buildShadowCard('radius=4\n轻阴影', 4, 10, 10, 'rgba(0, 0, 0, 0.25)')

          // --- 深度 2:中阴影(radius = 15) ---
          this.buildShadowCard('radius=15\n中阴影', 15, 10, 10, 'rgba(0, 0, 0, 0.30)')

          // --- 深度 3:重阴影(radius = 40) ---
          this.buildShadowCard('radius=40\n重阴影', 40, 10, 10, 'rgba(0, 0, 0, 0.35)')
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
        .justifyContent(FlexAlign.SpaceAround)

        // =========================================================
        // 第二部分:阴影方向对比(固定模糊半径,变化偏移方向)
        // =========================================================
        Text('二、阴影方向(offset)对比 —— 半径固定 20')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%')
          .padding({ left: 12 })
          .margin({ top: 8 })

        // 第一行:右下 / 左下
        Row({ space: 16 }) {
          this.buildShadowCard('↘ 右下\n(15, 15)', 20, 15, 15, 'rgba(0, 0, 0, 0.35)')
          this.buildShadowCard('↙ 左下\n(-15, 15)', 20, -15, 15, 'rgba(0, 0, 0, 0.35)')
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
        .justifyContent(FlexAlign.SpaceAround)

        // 第二行:右上 / 左上
        Row({ space: 16 }) {
          this.buildShadowCard('↗ 右上\n(15, -15)', 20, 15, -15, 'rgba(0, 0, 0, 0.35)')
          this.buildShadowCard('↖ 左上\n(-15, -15)', 20, -15, -15, 'rgba(0, 0, 0, 0.35)')
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
        .justifyContent(FlexAlign.SpaceAround)

        // =========================================================
        // 第三部分:纯方向(仅一个方向偏移)
        // =========================================================
        Text('三、单一方向偏移 —— 半径固定 15')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%')
          .padding({ left: 12 })
          .margin({ top: 8 })

        Row({ space: 12 }) {
          this.buildShadowCard('↓ 下\n(0, 20)', 15, 0, 20, 'rgba(0, 0, 0, 0.30)')
          this.buildShadowCard('→ 右\n(20, 0)', 15, 20, 0, 'rgba(0, 0, 0, 0.30)')
          this.buildShadowCard('↑ 上\n(0, -20)', 15, 0, -20, 'rgba(0, 0, 0, 0.30)')
          this.buildShadowCard('← 左\n(-20, 0)', 15, -20, 0, 'rgba(0, 0, 0, 0.30)')
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
        .justifyContent(FlexAlign.SpaceAround)

        // =========================================================
        // 第四部分:自定义交互演示区
        // =========================================================
        Text('四、自定义阴影参数(拖动滑块实时调整)')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%')
          .padding({ left: 12 })
          .margin({ top: 8 })

        // --- 自定义阴影预览卡片 ---
        Column() {
          // 预览卡片本身
          Row() {
            Column() {
              Text('预览区')
                .fontSize(20)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FF6600')
              Text(`radius = ${this.currentRadius.toFixed(0)}`)
                .fontSize(14)
                .fontColor('#333333')
                .margin({ top: 4 })
              Text(`offsetX = ${this.currentOffsetX.toFixed(0)}, offsetY = ${this.currentOffsetY.toFixed(0)}`)
                .fontSize(14)
                .fontColor('#333333')
            }
            .width(180)
            .height(120)
            .justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
          }
          .width(200)
          .height(140)
          .backgroundColor(Color.White)
          .borderRadius(16)
          .align(Alignment.Center)
          // ★★★ 核心:应用动态阴影(三个关键参数:模糊半径 + 方向偏移) ★★★
          .shadow({
            radius: this.currentRadius,
            offsetX: this.currentOffsetX,
            offsetY: this.currentOffsetY,
            color: 'rgba(0, 0, 0, 0.40)',
            fill: true
          })
          .margin({ top: 12, bottom: 8 })

          // --- 模糊半径滑块 ---
          Text(`模糊半径 (radius): ${this.currentRadius.toFixed(0)}`)
            .fontSize(14)
            .fontColor('#333333')
            .width('100%')
            .padding({ left: 16 })
          Slider({
            value: this.currentRadius,
            min: 0,
            max: 60,
            step: 1,
            style: SliderStyle.OutSet
          })
            .blockColor('#FF6600')
            .trackColor('#E0E0E0')
            .selectedColor('#FF6600')
            .width('90%')
            .onChange((value: number) => {
              this.currentRadius = value;
            })

          // --- 水平偏移滑块 ---
          Text(`水平偏移 (offsetX): ${this.currentOffsetX.toFixed(0)}`)
            .fontSize(14)
            .fontColor('#333333')
            .width('100%')
            .padding({ left: 16 })
          Slider({
            value: this.currentOffsetX,
            min: -40,
            max: 40,
            step: 1,
            style: SliderStyle.OutSet
          })
            .blockColor('#0066FF')
            .trackColor('#E0E0E0')
            .selectedColor('#0066FF')
            .width('90%')
            .onChange((value: number) => {
              this.currentOffsetX = value;
            })

          // --- 垂直偏移滑块 ---
          Text(`垂直偏移 (offsetY): ${this.currentOffsetY.toFixed(0)}`)
            .fontSize(14)
            .fontColor('#333333')
            .width('100%')
            .padding({ left: 16 })
          Slider({
            value: this.currentOffsetY,
            min: -40,
            max: 40,
            step: 1,
            style: SliderStyle.OutSet
          })
            .blockColor('#00AA00')
            .trackColor('#E0E0E0')
            .selectedColor('#00AA00')
            .width('90%')
            .onChange((value: number) => {
              this.currentOffsetY = value;
            })

          // --- 预设按钮组 ---
          Text('快速预设:')
            .fontSize(14)
            .fontColor('#333333')
            .width('100%')
            .padding({ left: 16, top: 8 })
          Row({ space: 8 }) {
            this.buildPresetButton('底部投影', 15, 0, 20)
            this.buildPresetButton('右侧投影', 15, 20, 0)
            this.buildPresetButton('扩散模糊', 50, 0, 0)
            this.buildPresetButton('左上光晕', 30, -20, -20)
          }
          .width('100%')
          .padding({ left: 16, right: 16 })
          .justifyContent(FlexAlign.Start)
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F5F5F5')
        .borderRadius(16)
        .margin({ left: 12, right: 12 })

        // =========================================================
        // 第五部分:阴影创建方式对比
        // =========================================================
        Text('五、ShadowStyle 预置样式(ArkUI 内置)')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor('#333333')
          .width('100%')
          .padding({ left: 12 })
          .margin({ top: 8 })

        Row({ space: 12 }) {
          // ★★★ ShadowStyle.OUTER_DEFAULT_SM:小型外阴影 ★★★
          this.buildStyleCard('OUTER_\nDEFAULT_SM', ShadowStyle.OUTER_DEFAULT_SM)

          // ★★★ ShadowStyle.OUTER_DEFAULT_MD:中型外阴影 ★★★
          this.buildStyleCard('OUTER_\nDEFAULT_MD', ShadowStyle.OUTER_DEFAULT_MD)

          // ★★★ ShadowStyle.OUTER_DEFAULT_LG:大型外阴影 ★★★
          this.buildStyleCard('OUTER_\nDEFAULT_LG', ShadowStyle.OUTER_DEFAULT_LG)
        }
        .width('100%')
        .padding({ left: 12, right: 12 })
        .justifyContent(FlexAlign.SpaceAround)

        // =========================================================
        // 底部说明
        // =========================================================
        Column({ space: 6 }) {
          Text('📌 布局要点总结')
            .fontSize(15)
            .fontWeight(FontWeight.Bold)
          Text('1. .shadow({ radius, offsetX, offsetY, color }) 精确控制每个参数')
            .fontSize(13)
            .fontColor('#555555')
          Text('2. radius(模糊半径)决定阴影的扩散范围,值越大阴影越柔和/扩散')
            .fontSize(13)
            .fontColor('#555555')
          Text('3. offsetX / offsetY 控制阴影偏移方向(正负决定方向)')
            .fontSize(13)
            .fontColor('#555555')
          Text('4. ShadowStyle 提供三档内置预设(SM / MD / LG)')
            .fontSize(13)
            .fontColor('#555555')
          Text('5. 阴影颜色支持透明度(如 rgba(0,0,0,0.3)),叠加更自然')
            .fontSize(13)
            .fontColor('#555555')
        }
        .width('100%')
        .padding(16)
        .backgroundColor('#FFF8E1')
        .borderRadius(12)
        .margin({ left: 12, right: 12, bottom: 30 })
      }
      .width('100%')
    }
    .backgroundColor('#F0F0F0')
    .width('100%')
    .height('100%')
  }

  // ================================================================
  //  辅助构建方法:生成一张阴影对比卡片
  //  @param label   - 卡片标签文字
  //  @param radius  - 阴影模糊半径
  //  @param ox      - 水平偏移量
  //  @param oy      - 垂直偏移量
  //  @param color   - 阴影颜色
  // ================================================================
  @Builder
  buildShadowCard(label: string, radius: number, ox: number, oy: number, color: string) {
    Column() {
      // 上色块(视觉参照物)
      Column()
        .width(60)
        .height(60)
        .backgroundColor('#FF6600')
        .borderRadius(12)
        // ★★★ 核心:对矩形应用自定义阴影 ★★★
        .shadow({
          radius: radius,       // 模糊半径(深度)
          offsetX: ox,          // 水平偏移方向
          offsetY: oy,          // 垂直偏移方向
          color: color,         // 阴影颜色
          fill: true
        })

      // 参数标签
      Text(label)
        .fontSize(11)
        .fontColor('#444444')
        .textAlign(TextAlign.Center)
        .margin({ top: 8 })
        .lineHeight(16)
    }
    .padding(8)
  }

  // ================================================================
  //  辅助构建方法:使用 ShadowStyle 预置样式生成卡片
  //  @param label - 卡片标签
  //  @param style - ShadowStyle 枚举值
  // ================================================================
  @Builder
  buildStyleCard(label: string, style: ShadowStyle) {
    Column() {
      Column()
        .width(60)
        .height(60)
        .backgroundColor('#0066FF')
        .borderRadius(12)
        // ★★★ 核心:使用预置 ShadowStyle 枚举,无需手动指定参数 ★★★
        .shadow(style)

      Text(label)
        .fontSize(11)
        .fontColor('#444444')
        .textAlign(TextAlign.Center)
        .margin({ top: 8 })
        .lineHeight(16)
    }
    .padding(8)
  }

  // ================================================================
  //  辅助构建方法:快速预设按钮
  // ================================================================
  @Builder
  buildPresetButton(label: string, radius: number, ox: number, oy: number) {
    Button(label)
      .fontSize(12)
      .fontWeight(FontWeight.Regular)
      .height(32)
      .backgroundColor('#FFFFFF')
      .fontColor('#333333')
      .borderRadius(16)
      .padding({ left: 12, right: 12 })
      .border({ width: 1, color: '#DDDDDD' })
      .onClick(() => {
        this.currentRadius = radius;
        this.currentOffsetX = ox;
        this.currentOffsetY = oy;
        this.selectedPreset = label;
      })
  }
}

五、代码架构与设计模式分析

5.1 @Builder 装饰器的妙用

在 Index.ets 中,我们大量使用了 @Builder 装饰器来封装可复用的 UI 片段。这是 ArkTS 声明式编程中非常重要的一个设计模式。

@Builder
buildShadowCard(label: string, radius: number, ox: number, oy: number, color: string) {
  // ... 卡片 UI 结构
}

@Builder
buildStyleCard(label: string, style: ShadowStyle) {
  // ... 预置样式卡片
}

@Builder
buildPresetButton(label: string, radius: number, ox: number, oy: number) {
  // ... 预设按钮
}

为什么使用 @Builder 而不是自定义组件?

  • @Builder 更轻量,不需要声明新的 @Component struct,适合封装纯粹的 UI 片段。
  • @Builder 方法可以直接访问外层组件的状态变量,无需通过 @Prop 传递。
  • 当同一段 UI 结构在页面中多处复用时,@Builder 是最好的选择。

为什么使用 @Builder 而不是普通函数?

  • @Builder 方法内部可以使用链式调用的 UI 语法(.width().height() 等),而普通函数不能。
  • @Builder 方法支持状态变量的自动追踪和更新,普通函数不具备此能力。

5.2 状态驱动的参数绑定

我们在自定义交互区使用了四个 @State 变量:

@State currentRadius: number = 20;
@State currentOffsetX: number = 10;
@State currentOffsetY: number = 10;
@State selectedPreset: string = '默认';

典型的数据流如下:

用户拖动 Slider → onChange 回调 → 更新 @State 变量
    → ArkUI 框架自动检测状态变化 → 重新调用 build() 方法
    → 更新 .shadow() 的参数 → 重新渲染阴影效果

这个响应式数据流是 ArkUI 框架的核心优势——开发者只需要维护状态数据,UI 的更新由框架自动处理,无需手动操作 DOM 或 Canvas。

5.3 Scroll + Column 的滚动布局模式

整个页面使用 Scroll 包裹 Column 的布局模式,这是 ArkUI 中最基础也最实用的滚动页面布局方案:

Scroll()           ← 提供可滚动的容器
  Column()         ← 垂直方向排列子组件
    Text(...)      ← 标题
    Row(...)       ← 横向排列的卡片组
    Text(...)      ← 下一节标题
    Row(...)       ← 下一组卡片
    ...            ← 以此类推

这种布局模式的优势在于:

  1. 良好的扩展性:新增内容只需要在 Column 中添加新的子组件即可。
  2. 自然的滚动体验:内容超出屏幕高度时自动激活滚动。
  3. 灵活的组合能力:Column 内部可以嵌套 Row、Grid、Flex 等多种布局容器。

六、阴影效果的设计原则与最佳实践

6.1 层次感设计:建立清晰的视觉层级

在 UI 设计中,阴影的主要作用是建立视觉层级(Visual Hierarchy)。通过为不同重要性的元素赋予不同的阴影深度,可以引导用户的视线按照设计意图流动。

推荐的层级分配方案:

层级 元素类型 推荐阴影参数
第一层(基础层) 页面背景、列表项 无阴影或极浅阴影(radius: 2~4)
第二层(内容层) 卡片、按钮、输入框 浅阴影(radius: 4~10, offsetY: 2~4)
第三层(浮动层) 下拉菜单、工具提示 中阴影(radius: 10~20, offsetY: 4~8)
第四层(模态层) 对话框、底部弹窗 重阴影(radius: 20~40, offsetY: 8~16)
第五层(顶层) Toast、Snackbar 极重阴影(radius: 40+, offsetY: 10+)

6.2 一致性原则:统一光源方向

在同一个页面或应用中,阴影的偏移方向应当保持一致。推荐的做法是:

  1. 默认使用正下方偏移offsetX: 0, offsetY: positive。这模拟了环境光从正上方照射的场景,与 Material Design 的 elevation 系统一致。
  2. 如果使用斜向偏移:统一使用 offsetX > 0, offsetY > 0(右下),模拟光源位于左上角的常见场景。
  3. 避免光源方向跳变:不要在一个卡片上使用右下阴影,在另一个卡片上使用左下阴影。这会造成视觉上的不协调感。

6.3 阴影的真实感:参数联调技巧

要获得真实感强的阴影效果,需要注意各参数之间的配合:

  1. radius 与 alpha 的负相关

    • radius 较小时(4~8),alpha 可以稍高(0.25~0.35)
    • radius 较大时(30+),alpha 应当降低(0.15~0.25)
    • 这是因为大半径的阴影扩散范围大,如果颜色太深会显得"脏"
  2. offset 与 radius 的正相关

    • radius 越大,阴影越扩散,offset 通常也应当适当增大
    • 合理的比例大约是 offset = radius × 0.3 ~ 0.5
  3. 多层阴影叠加

    • 在复杂的 UI 场景中,单层阴影可能不足以表现真实感
    • 可以考虑使用多个组件嵌套,每层使用不同的阴影参数
    • 例如:最外层用大半径低透明度阴影模拟环境光,内层用小半径高透明度阴影模拟接触阴影

6.4 避免常见的阴影误区

误区一:半径过大导致模糊失真

radius 超过组件尺寸的 50% 时,阴影可能会扩散到组件外部很远,看起来不像"阴影"而更像"光晕"。对于小尺寸元素(如图标、小按钮),建议 radius 不超过 15。

误区二:颜色过深导致"脏"效果

阴影颜色的 alpha 值不宜超过 0.5。过深的阴影会让界面显得沉重、不清爽。推荐的范围是 0.15 ~ 0.40。

误区三:偏移过大导致阴影与组件脱离

当 offset 远大于 radius 时,阴影会与组件明显分离,看起来像是"两个独立的物体"。建议 offset 不超过 radius 的 1.5 倍。

误区四:忽略 border-radius 对阴影的影响

当组件设置了 borderRadius 时,阴影的形状会跟随圆角变化。确保组件的 borderRadius 不为 0(除非你明确想要直角阴影),因为圆角阴影更自然。


七、ShadowStyle 内置预设的源码解读

在 ArkUI 框架内部,ShadowStyle 枚举映射到一组预定义的阴影参数。虽然没有官方文档给出精确的数值,但根据框架的行为和实际渲染效果,我们可以大致推断出各预设值对应的参数范围:

ShadowStyle 等效 radius 等效 offsetX 等效 offsetY 适用场景
OUTER_DEFAULT_XS ~2 ~0 ~1 图标、小标签
OUTER_DEFAULT_SM ~6 ~0 ~2 按钮、小型卡片
OUTER_DEFAULT_MD ~12 ~0 ~4 中型卡片、下拉菜单
OUTER_DEFAULT_LG ~24 ~0 ~8 对话框、模态弹窗

使用预置样式的优势在于:

  1. 一致性保证:整个应用中的所有阴影效果使用同一套预设参数,视觉风格统一。
  2. 开发效率:一行代码 .shadow(ShadowStyle.OUTER_DEFAULT_MD) 即可获得合理的阴影效果。
  3. 主题适配:在深色模式下,ShadowStyle 会自动调整阴影的透明度和颜色,而手动设置的参数需要开发者自行适配。

但是,预置样式的缺点是无法控制阴影方向——所有 ShadowStyle 都是正下方偏移(offsetX = 0, offsetY > 0)。如果你的设计需要斜向阴影或特殊方向,必须使用 ShadowOptions 方式手动配置。


八、性能优化与注意事项

8.1 阴影的渲染性能开销

阴影效果的渲染需要额外的 GPU 计算资源。在 HarmonyOS NEXT 中,阴影的渲染开销主要来自两个方面:

  1. 模糊运算radius 越大,GPU 需要计算的模糊核范围就越大,性能开销也越大。
  2. 叠加区域:阴影覆盖的像素区域越大,需要处理的片元就越多。

性能优化建议:

  1. 避免大量组件同时使用大半径阴影:如果一个页面中有数十个卡片都使用了 radius=40 的阴影,可能会导致帧率下降。
  2. 使用 ShadowStyle 预设:框架对预设值可能有内部优化,性能优于手动设置的大半径阴影。
  3. 列表中的阴影优化:在 List 组件中使用 LazyForEach 延迟加载时,为列表项添加阴影可能会影响滑动性能。建议只在选中态或按压态添加阴影。
  4. 测试不同设备的性能表现:在低端设备上,适当减小 radius 值以保证 60fps 的流畅度。

8.2 深色模式下的阴影适配

在深色模式(Dark Mode)下,由于背景颜色变深,阴影效果需要做相应的调整:

// 浅色模式
.shadow({
  radius: 12,
  offsetX: 0,
  offsetY: 4,
  color: 'rgba(0, 0, 0, 0.30)',
  fill: true
})

// 深色模式
.shadow({
  radius: 12,
  offsetX: 0,
  offsetY: 4,
  color: 'rgba(0, 0, 0, 0.50)',  // 透明度需要提高
  fill: true
})

在深色背景上,黑色阴影几乎不可见,因此需要:

  • 提高 alpha 值(从 0.30 提高到 0.50 甚至更高)
  • 或者使用浅色阴影(如 rgba(255, 255, 255, 0.08))模拟"发光"效果
  • 使用 @Styles@Extend 定义不同主题下的阴影样式

8.3 避免阴影的过度使用

虽然阴影能够增强 UI 的层次感,但过度使用也会带来负面影响:

  1. 视觉噪音:大量杂乱的阴影会让界面看起来"脏"和"杂乱"。
  2. 层级混乱:每个元素都有阴影会让用户分不清哪些元素是真正"浮起来"的。
  3. 性能负担:如上所述,大量阴影会影响渲染性能。

最佳实践:阴影应当是"信号"而非"装饰"。只在需要强调层级差异的地方使用阴影,不要为了视觉效果而给每个元素都加上阴影。


九、与其他 UI 框架的阴影实现对比

9.1 与 CSS box-shadow 的对比

CSS 中的 box-shadow 是前端开发中最常用的阴影方案:

box-shadow: offset-x offset-y blur-radius spread-radius color;

与 ArkUI 的 .shadow() 相比:

  • ArkUI 没有 spread-radius(扩散半径)参数,而是通过 fill 布尔值控制内部填充。
  • CSS 的 box-shadow 支持多层阴影(逗号分隔),ArkUI 目前只支持单层。
  • CSS 的 box-shadow 中的 inset 关键字可以实现内阴影,ArkUI 需要通过其他方式模拟。

9.2 与 Flutter 的对比

Flutter 中使用 BoxDecorationboxShadow 属性或 Material widget:

Container(
  decoration: BoxDecoration(
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.3),
        blurRadius: 12,
        offset: Offset(0, 4),
      ),
    ],
  ),
)

与 ArkUI 的相似性较高,都支持 colorblurRadius(相当于 radius)、offset。Flutter 也支持多层阴影。

9.3 与 SwiftUI 的对比

SwiftUI 中使用 .shadow() 修饰符:

Text("Hello")
  .shadow(color: .black.opacity(0.3), radius: 12, x: 0, y: 4)

与 ArkUI 的 .shadow() API 设计惊人的一致,都使用链式调用,参数名也几乎相同(radius、x/y offset、color)。这体现了跨平台声明式 UI 框架在设计理念上的趋同性。


十、常见问题与解决方案(FAQ)

Q1: 阴影没有显示出来

可能的原因:

  1. 组件没有设置背景色:透明背景的组件上阴影不可见。阴影是渲染在组件背景之下的,如果背景是透明的,阴影会被"穿透"。

    解决方案:为组件设置 backgroundColor,哪怕是白色也能让阴影显现。

  2. radius 为 0:当 radius = 0 时,阴影没有模糊效果,如果 offset 也为 0,阴影完全不可见。

    解决方案:设置 radius > 0,或者设置非零的 offset。

  3. 颜色 alpha 为 0:如果设置了颜色为完全透明(如 rgba(0,0,0,0)),阴影当然看不见。

    解决方案:使用带透明度的颜色,如 rgba(0, 0, 0, 0.3)

  4. 组件被裁剪了:如果组件的父容器设置了 clip(true)overflow: Hidden,阴影可能被裁剪到父容器之外。

    解决方案:检查父容器是否裁剪内容,移除不必要的裁剪设置。

Q2: 阴影过于生硬不自然

可能的原因:

  1. radius 太小:radius < 3 时阴影几乎不模糊,看起来像一个"复制层"。

    解决方案:增大 radius 值,建议从 8 开始调整。

  2. 颜色 alpha 太高:alpha > 0.5 的阴影会显得沉重。

    解决方案:降低 alpha 值到 0.15~0.35 之间。

  3. offset 与 radius 比例失调:offset 远大于 radius 时,阴影与组件脱离,显得不自然。

    解决方案:保持 offset 在 radius 的 0.3~1.0 倍之间。

Q3: 如何实现多层阴影?

ArkUI 目前不支持在单个 .shadow() 调用中叠加多层阴影。但可以通过组件嵌套来模拟:

// 模拟双层阴影:外层大半径环境阴影 + 内层小半径接触阴影
Column() {
  // 内层:实际的 UI 内容
  Column() {
    // 内容...
  }
  .width(200)
  .height(120)
  .backgroundColor(Color.White)
  .borderRadius(16)
  // 第二层阴影(接触阴影)
  .shadow({
    radius: 6,
    offsetX: 0,
    offsetY: 2,
    color: 'rgba(0, 0, 0, 0.25)',
    fill: true
  })
}
.padding(8)  // 给外层阴影留出空间
// 第一层阴影(环境阴影)
.shadow({
  radius: 30,
  offsetX: 0,
  offsetY: 10,
  color: 'rgba(0, 0, 0, 0.15)',
  fill: true
})

Q4: 文本组件(Text)的阴影如何使用?

Text 组件同样支持 .shadow() 方法,但与背景组件的阴影效果不同——文字的阴影作用于文字本身:

Text('带阴影的文字')
  .fontSize(24)
  .fontColor(Color.Black)
  .shadow({
    radius: 4,
    offsetX: 2,
    offsetY: 2,
    color: 'rgba(0, 0, 0, 0.3)'
  })

这种方式适合制作标题文字的浮雕效果或发光效果。


十一、扩展应用:将阴影融入实际业务场景

11.1 卡片列表中的阴影应用

在实际的电商或内容类应用中,卡片列表是最常见的 UI 模式。合适的阴影能够让卡片从背景中"浮起",增强可点击性的暗示。

@Builder
productCard(item: ProductItem) {
  Column() {
    Image(item.imageUrl)
      .width('100%')
      .height(160)
      .objectFit(ImageFit.Cover)
    Column({ space: 4 }) {
      Text(item.name)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
      Text(`¥${item.price}`)
        .fontSize(14)
        .fontColor('#FF6600')
    }
    .padding(12)
    .width('100%')
  }
  .width('100%')
  .backgroundColor(Color.White)
  .borderRadius(12)
  // 卡片阴影:适中深度 + 仅向下偏移
  .shadow({
    radius: 12,
    offsetX: 0,
    offsetY: 4,
    color: 'rgba(0, 0, 0, 0.12)',
    fill: true
  })
  // 点击反馈:按下时加深阴影(可通过状态变量实现)
}

11.2 浮动操作按钮(FAB)的阴影

浮动操作按钮是典型的"高 elevation"元素,需要明显的阴影来强调其"浮动"状态:

Button() {
  Image($r('app.media.ic_add'))
    .width(24)
    .height(24)
}
.width(56)
.height(56)
.backgroundColor('#FF6600')
.borderRadius(28)
// FAB 需要较重的阴影,暗示其 elevation 最高
.shadow({
  radius: 24,
  offsetX: 0,
  offsetY: 8,
  color: 'rgba(255, 102, 0, 0.35)',  // 使用与按钮同色系的阴影
  fill: true
})

11.3 对话框与弹窗的阴影

模态对话框在视觉上需要明显区别于页面背景,因此需要使用最重的阴影效果:

Column() {
  // 对话框内容...
}
.width(300)
.backgroundColor(Color.White)
.borderRadius(20)
// 对话框阴影:大半径 + 大偏移,视觉上浮起最高
.shadow({
  radius: 48,
  offsetX: 0,
  offsetY: 16,
  color: 'rgba(0, 0, 0, 0.25)',
  fill: true
})

十二、总结与展望

12.1 核心要点回顾

通过本文的详细讲解和完整的实战代码,我们深入了解了鸿蒙原生 ArkTS 中 .shadow() API 的方方面面:

  1. API 基础.shadow() 接受 ShadowOptions 对象或 ShadowStyle 枚举,用于为组件添加阴影效果。
  2. 四个核心参数radius(模糊半径,控制深度感)、offsetX/offsetY(控制阴影方向)、color(控制阴影颜色和透明度)。
  3. 两种使用方式ShadowOptions 方式支持精确控制每个参数,适合自定义设计;ShadowStyle 方式使用框架内置预设,适合快速开发和保持一致性。
  4. 设计原则:保持光源方向一致、根据元素层级选择阴影深度、radius 与 alpha 配合调整以获得真实感。
  5. 性能考量:避免大量大半径阴影叠加,注意深色模式的适配。
  6. 实战应用:通过 @Builder 封装复用代码,使用 @State 驱动参数动态变化,Scroll + Column 构建滚动页面。

12.2 对未来的展望

随着 HarmonyOS NEXT 生态的不断发展,ArkUI 框架也在持续进化。我们有理由期待未来版本的 .shadow() API 会带来更多功能:

  • 内阴影(Inset Shadow)支持:实现类似 CSS box-shadow: inset 的向内阴影效果。
  • 多层阴影:在单个 .shadow() 调用中支持叠加多层阴影。
  • 动画阴影:支持阴影参数的平滑过渡动画。
  • 阴影生成器工具:DevEco Studio 中可能集成可视化的阴影参数调节工具。

12.3 写在最后

阴影看似是一个很小的 UI 细节,但它在提升用户体验方面发挥着不可替代的作用。一个精心设计的阴影系统,能够让你的应用在视觉上显得更加专业、精致和有层次感。

正如著名设计师 Dieter Rams 所说:“Good design is as little design as possible.”(好的设计是尽可能少的设计。)在阴影的使用上也是如此——恰到好处的阴影,胜过华丽的堆砌。

希望本文能够帮助你在 HarmonyOS 开发中更好地运用 .shadow() API,打造出既美观又高性能的鸿蒙原生应用。如果你有任何问题或想法,欢迎在评论区留言交流!


参考资源


本文基于 HarmonyOS NEXT API 24 编写,示例代码在 DevEco Studio 中编译通过并运行验证。文中所有观点仅代表作者个人,如有不当之处欢迎指正。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐