【共创季稿事节】鸿蒙ArkTS阴影效果深度解析——shadow的深度与方向控制
鸿蒙原生 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 —— 偏移方向(光源位置的暗示)
offsetX 和 offsetY 共同决定了阴影的投射方向,它们在视觉上暗示了光源的位置:
- offsetX > 0, offsetY > 0:阴影向右下方偏移 → 光源位于左上方。这是最自然、最常用的方向,模拟了日常环境中顶灯从左上角照射的效果。
- offsetX < 0, offsetY > 0:阴影向左下方偏移 → 光源位于右上方。
- offsetX > 0, offsetY < 0:阴影向右上方偏移 → 光源位于左下方。这种"底部光照"效果较少见,可用于营造特殊的视觉氛围,如舞台灯光。
- offsetX = 0, offsetY > 0:阴影仅向下偏移 → 光源位于正上方。这是 Material Design 推荐的默认方向,模拟了环境中环境光从正上方照射的效果。
在同一个应用或页面中,建议保持阴影方向的一致性,避免给用户造成"光源位置混乱"的不协调感。
color —— 阴影颜色的选择策略
阴影颜色的选择直接影响最终的真实感。推荐的策略包括:
- 使用带透明度的黑色:
rgba(0, 0, 0, 0.2)~rgba(0, 0, 0, 0.4)是最安全、最通用的选择。黑色叠加任何背景色都会产生自然的阴影效果。 - 根据背景色调整透明度:浅色背景上透明度可以稍高(如 0.3),深色背景上透明度需降低(如 0.15),否则阴影会过于突兀。
- 色彩阴影:在某些场景下,可以使用与主题色同色系但更暗的颜色作为阴影色。例如,在橙色卡片下使用
rgba(200, 80, 0, 0.3)会产生"彩色阴影"效果,视觉上更加协调。 - 透明度与 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(...) ← 下一组卡片
... ← 以此类推
这种布局模式的优势在于:
- 良好的扩展性:新增内容只需要在 Column 中添加新的子组件即可。
- 自然的滚动体验:内容超出屏幕高度时自动激活滚动。
- 灵活的组合能力: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 一致性原则:统一光源方向
在同一个页面或应用中,阴影的偏移方向应当保持一致。推荐的做法是:
- 默认使用正下方偏移:
offsetX: 0, offsetY: positive。这模拟了环境光从正上方照射的场景,与 Material Design 的 elevation 系统一致。 - 如果使用斜向偏移:统一使用
offsetX > 0, offsetY > 0(右下),模拟光源位于左上角的常见场景。 - 避免光源方向跳变:不要在一个卡片上使用右下阴影,在另一个卡片上使用左下阴影。这会造成视觉上的不协调感。
6.3 阴影的真实感:参数联调技巧
要获得真实感强的阴影效果,需要注意各参数之间的配合:
-
radius 与 alpha 的负相关:
- radius 较小时(4~8),alpha 可以稍高(0.25~0.35)
- radius 较大时(30+),alpha 应当降低(0.15~0.25)
- 这是因为大半径的阴影扩散范围大,如果颜色太深会显得"脏"
-
offset 与 radius 的正相关:
- radius 越大,阴影越扩散,offset 通常也应当适当增大
- 合理的比例大约是 offset = radius × 0.3 ~ 0.5
-
多层阴影叠加:
- 在复杂的 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 | 对话框、模态弹窗 |
使用预置样式的优势在于:
- 一致性保证:整个应用中的所有阴影效果使用同一套预设参数,视觉风格统一。
- 开发效率:一行代码
.shadow(ShadowStyle.OUTER_DEFAULT_MD)即可获得合理的阴影效果。 - 主题适配:在深色模式下,ShadowStyle 会自动调整阴影的透明度和颜色,而手动设置的参数需要开发者自行适配。
但是,预置样式的缺点是无法控制阴影方向——所有 ShadowStyle 都是正下方偏移(offsetX = 0, offsetY > 0)。如果你的设计需要斜向阴影或特殊方向,必须使用 ShadowOptions 方式手动配置。
八、性能优化与注意事项
8.1 阴影的渲染性能开销
阴影效果的渲染需要额外的 GPU 计算资源。在 HarmonyOS NEXT 中,阴影的渲染开销主要来自两个方面:
- 模糊运算:
radius越大,GPU 需要计算的模糊核范围就越大,性能开销也越大。 - 叠加区域:阴影覆盖的像素区域越大,需要处理的片元就越多。
性能优化建议:
- 避免大量组件同时使用大半径阴影:如果一个页面中有数十个卡片都使用了 radius=40 的阴影,可能会导致帧率下降。
- 使用 ShadowStyle 预设:框架对预设值可能有内部优化,性能优于手动设置的大半径阴影。
- 列表中的阴影优化:在
List组件中使用LazyForEach延迟加载时,为列表项添加阴影可能会影响滑动性能。建议只在选中态或按压态添加阴影。 - 测试不同设备的性能表现:在低端设备上,适当减小 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 的层次感,但过度使用也会带来负面影响:
- 视觉噪音:大量杂乱的阴影会让界面看起来"脏"和"杂乱"。
- 层级混乱:每个元素都有阴影会让用户分不清哪些元素是真正"浮起来"的。
- 性能负担:如上所述,大量阴影会影响渲染性能。
最佳实践:阴影应当是"信号"而非"装饰"。只在需要强调层级差异的地方使用阴影,不要为了视觉效果而给每个元素都加上阴影。
九、与其他 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 中使用 BoxDecoration 的 boxShadow 属性或 Material widget:
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 12,
offset: Offset(0, 4),
),
],
),
)
与 ArkUI 的相似性较高,都支持 color、blurRadius(相当于 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: 阴影没有显示出来
可能的原因:
-
组件没有设置背景色:透明背景的组件上阴影不可见。阴影是渲染在组件背景之下的,如果背景是透明的,阴影会被"穿透"。
解决方案:为组件设置
backgroundColor,哪怕是白色也能让阴影显现。 -
radius 为 0:当 radius = 0 时,阴影没有模糊效果,如果 offset 也为 0,阴影完全不可见。
解决方案:设置 radius > 0,或者设置非零的 offset。
-
颜色 alpha 为 0:如果设置了颜色为完全透明(如
rgba(0,0,0,0)),阴影当然看不见。解决方案:使用带透明度的颜色,如
rgba(0, 0, 0, 0.3)。 -
组件被裁剪了:如果组件的父容器设置了
clip(true)或overflow: Hidden,阴影可能被裁剪到父容器之外。解决方案:检查父容器是否裁剪内容,移除不必要的裁剪设置。
Q2: 阴影过于生硬不自然
可能的原因:
-
radius 太小:radius < 3 时阴影几乎不模糊,看起来像一个"复制层"。
解决方案:增大 radius 值,建议从 8 开始调整。
-
颜色 alpha 太高:alpha > 0.5 的阴影会显得沉重。
解决方案:降低 alpha 值到 0.15~0.35 之间。
-
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 的方方面面:
- API 基础:
.shadow()接受ShadowOptions对象或ShadowStyle枚举,用于为组件添加阴影效果。 - 四个核心参数:
radius(模糊半径,控制深度感)、offsetX/offsetY(控制阴影方向)、color(控制阴影颜色和透明度)。 - 两种使用方式:
ShadowOptions方式支持精确控制每个参数,适合自定义设计;ShadowStyle方式使用框架内置预设,适合快速开发和保持一致性。 - 设计原则:保持光源方向一致、根据元素层级选择阴影深度、radius 与 alpha 配合调整以获得真实感。
- 性能考量:避免大量大半径阴影叠加,注意深色模式的适配。
- 实战应用:通过 @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 开发者文档 - ArkUI 阴影效果
- HarmonyOS 开发者文档 - ShadowOptions 接口说明
- Material Design - Elevation & Shadows
- 本文完整示例代码:
entry/src/main/ets/pages/Index.ets
本文基于 HarmonyOS NEXT API 24 编写,示例代码在 DevEco Studio 中编译通过并运行验证。文中所有观点仅代表作者个人,如有不当之处欢迎指正。
更多推荐



所有评论(0)