鸿蒙新特性:ArkUI 视觉效果进阶 — 渐变、卡片与仪表盘设计
很多 ArkUI 开发者陷入"能用就行"——白底黑字、单调纯色、矩形卡片。本文把 HarmonyOS NEXT 的 LinearGradient(线性渐变)、Progress(进度条)和卡片设计组合成一个精美的"数据看板",展示如何用声明式 API 做出真正的视觉品质感。
一、为什么需要关注视觉效果?
声明式 UI 框架(ArkUI、SwiftUI、Flutter)都有一个共同特点:默认样式很朴素。纯色背景、直角矩形、系统字体——这让开发者的第一个 MVP 看起来"够用",但离"好看"还有距离。
但用户对"好看"的感知非常敏锐。一个渐变背景的卡片比纯色的看起来"更专业",一个有动画的进度条比静态的"更流畅",一个圆角阴影的白卡片比纯文本"更有品质感"。
ArkUI 在视觉效果上提供了足够的 API,只是很多开发者没用到。 本文聚焦三个关键效果:
- LinearGradient — 线性渐变,把纯色变成流动的色彩
- Progress — 进度条,从百分比到色彩映射,传递"进展感"
- 卡片设计 — 圆角、间隙、底色的组合,形成信息层级

二、LinearGradient:渐变背景的使用
2.1 基本 API
Column() { ... }
.linearGradient({
angle: 135, // 渐变角度(0=右, 90=下, 180=左, 270=上)
colors: [
['#4FACFE', 0.0], // 起始色(位置 0%)
['#00F2FE', 1.0] // 结束色(位置 100%)
]
})
关键参数:
angle:渐变方向角度。0是从左到右,90是从上到下,135是从左上到右下(对角线渐变,最有立体感),180是从右到左,270是从下到上。colors:颜色数组,每项是[颜色字符串, 位置比例]。位置从 0.0 到 1.0,可以在中间插入更多颜色停靠点做出多色渐变。
2.2 角度选择的视觉效果
| 角度 | 方向 | 视觉效果 | 推荐场景 |
|---|---|---|---|
| 0 | 左→右 | 平淡的横向变化 | 横幅、标题栏 |
| 90 | 上→下 | 自然的纵向变化 | 页面背景、列表头 |
| 135 | 左上→右下 | 最具立体感和纵深感 | 卡片、按钮、统计块 |
| 180 | 右→左 | 倒置的横向变化 | 特殊强调 |
| 270 | 下→上 | 倒置的纵向变化 | 底部导航、页脚 |
对于统计卡片,135° 对角线渐变效果最好——它让卡片看起来有光影从左上角照射下来的立体感。
2.3 多色渐变
不像 CSS 的 linear-gradient 只有两个端点,ArkUI 的 colors 数组支持任意多个颜色停靠点:
.linearGradient({
angle: 180,
colors: [
['#1a1a2e', 0.0], // 0% — 深蓝黑
['#16213e', 0.5], // 50% — 深蓝
['#0f3460', 1.0] // 100% — 亮蓝
]
})
这给页面背景创造了丰富的色彩层次——从深色过渡到亮色,让整个页面的"品质感"大幅提升。
2.4 渐变 vs 纯色的感知差异
纯色卡片:
┌─────────────┐
│ ¥128,930 │ ← 视觉单调,像"表单"
│ 总收入 │
└─────────────┘
渐变卡片:
┌─────────────┐
│ ░░░░░░░░░░ │ ← 有光影流动,像"仪表盘"
│ ¥128,930░░ │
│ 总收入░░░░░ │
└─────────────┘
同样的布局、同样的字体——渐变版本的卡片看起来"贵"了很多。这就是为什么几乎所有现代 SaaS 产品(Stripe、Vercel、Linear)的仪表盘都在用渐变卡片。

三、Progress:进度条的正确用法
3.1 基本 API
Progress({ value: 75, total: 100, type: ProgressType.Linear })
.width('100%')
.height(8)
.color('#4FACFE') // 前景色(已完成的颜色)
.backgroundColor('#F0F0F0') // 背景色(轨道的颜色)
Progress 不是只能做文件上传进度条。在仪表盘中,它用来可视化任何"部分/整体"的数据关系——项目完成度、目标达成率、预算使用比例。
3.2 进度 → 色彩的语义映射
不要所有的进度条都用同一个颜色。用颜色传递"进展状态":
getProgressColor(percent: number): string {
if (percent >= 80) return '#43E97B'; // 绿色:快完成了,健康
if (percent >= 60) return '#4FACFE'; // 蓝色:正常推进
if (percent >= 40) return '#FFB347'; // 橙色:需要关注
return '#FA709A'; // 红色:滞后,有风险
}
这种"从颜色读取状态"的能力,是仪表盘区别于普通列表的核心优势——不需要读数字,扫一眼颜色就知道哪里有问题。
3.3 进度条 + 标签的组合模式
Column() {
Row() {
Text('App 3.0 重构') // 项目名称
Blank() // 撑开间距
Text('75%') // 百分比数字
.fontColor('#4FACFE') // 颜色和进度条一致
}
Progress({ value: 75, total: 100, type: ProgressType.Linear })
.width('100%').height(8)
.color('#4FACFE')
.backgroundColor('#F0F0F0')
Text('更新于 2 小时前') // 辅助信息
.fontColor('#999')
}
每一组进度卡片的完整信息层级:
- 第一行:项目名 + 百分比(主要信息)
- 第二行:进度条(视觉信号)
- 第三行:更新时间(辅助文本,灰色弱化)
四、卡片设计:信息层级的建立
4.1 纯白卡片 + 浅灰背景的组合
┌──────────────────────────────────┐
│ 浅灰页面背景 (#F2F3F7) │
│ ┌────────────┐ ┌────────────┐ │
│ │ 白卡片 │ │ 白卡片 │ │ ← 白色从灰底中"浮起来"
│ └────────────┘ └────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 白卡片 │ │
│ └────────────────────────────┘ │
└──────────────────────────────────┘
页面背景用浅灰(#F2F3F7 或 #F5F6FA),内容卡片用纯白(#FFFFFF),这是最经典的"分层"手法——白色卡片从灰色背景中"浮起来",用户自然知道卡片是一个独立的信息单元。
4.2 圆角半径的选择
.borderRadius(BorderRadius.SM) // 8vp — 小圆角,列表项
.borderRadius(BorderRadius.MD) // 12vp — 中圆角,普通卡片
.borderRadius(BorderRadius.LG) // 16vp — 大圆角,统计卡片、弹窗
统计卡片用大圆角(16vp),普通列表卡片用中圆角(12vp),标签/徽章用小圆角(8vp)。一致的圆角层级让设计看起来"有章法"。
4.3 内边距的一致性
// 统计卡片 — 大内边距,给数字呼吸空间
.padding({ top: Spacing.XL, bottom: Spacing.XL, left: Spacing.LG, right: Spacing.LG })
// 列表卡片 — 中等内边距
.padding({ top: Spacing.MD, bottom: Spacing.MD, left: Spacing.LG, right: Spacing.LG })
内边距不是随便给的。大的元素需要更大的 padding 来避免"拥挤感",小的元素需要较小的 padding 来避免"空洞感"。
五、Demo:数据看板
本 Demo 构建一个精美的数据看板——顶部深色渐变标题,中间四张渐变色统计卡片,下方项目进度和最近动态。
页面结构
DashboardPage (~200行)
├── Header(深色渐变背景 + 标题 + 日期)
│ └── linearGradient: 深蓝 → 深蓝 → 亮蓝(多色渐变)
├── Section 1:统计卡片(2×2 Grid)
│ ├── 总收入 — 蓝色渐变(angle: 135°)
│ ├── 活跃用户 — 紫色渐变(angle: 135°)
│ ├── 订单数 — 粉橙渐变(angle: 135°)
│ └── 增长率 — 绿色渐变(angle: 135°)
├── Section 2:项目进度
│ ├── App 3.0 重构(75%)
│ ├── 支付系统对接(60%)
│ ├── 官网营销页(92%)
│ ├── 用户画像系统(45%)
│ └── 自动化测试覆盖(68%)
│ └── progressColor: 绿/蓝/橙/红 按比例映射
└── Section 3:最近动态
└── 时间线式活动列表(彩色圆点 + 标题 + 时间)
统计卡片的设计细节
每张卡片包含三个元素:图标(emoji,30号字)、数值(加粗白色,22号字)、标签(半透明白色,12号字)。
Column() {
Text('💰').fontSize(30).margin({ bottom: Spacing.SM })
Text('¥128,930')
.fontSize(22).fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.margin({ bottom: 2 })
Text('总收入')
.fontSize(FontSize.CAPTION)
.fontColor('#FFFFFF99')
}
.width('100%')
.padding({ top: Spacing.XL, bottom: Spacing.XL,
left: Spacing.LG, right: Spacing.LG })
.borderRadius(BorderRadius.LG)
.linearGradient({
angle: 135,
colors: [['#4FACFE', 0.0], ['#00F2FE', 1.0]]
})
这些卡片使用 135° 对角线渐变——最经典的"光影从左上方来"的立体感。每张卡片用不同的渐变色对来区分:蓝→青(收入)、紫→粉(用户)、粉→黄(订单)、绿→青(增长)。
所有文字都是白色——在渐变背景上,白色文字通过半透明度(#FFFFFF99 = 60% 不透明度)来建立层级:
- 数值:纯白
#FFFFFF— 最重要的信息 - 标签:半透明白
#FFFFFF99— 辅助信息,视觉上后退
项目进度的色彩映射
getProgressColor(percent: number): string {
if (percent >= 80) return '#43E97B'; // 绿色:接近完成
if (percent >= 60) return '#4FACFE'; // 蓝色:正常
if (percent >= 40) return '#FFB347'; // 橙色:关注
return '#FA709A'; // 红色:落后
}
这不是随意的颜色映射——它利用了颜色本身的语义(绿=好,红=差)来让用户不看数字就能感知状态。
活动时间线
每条活动用彩色圆点 + 标题 + 时间的横向布局:
Row() {
Row().width(10).height(10).borderRadius(5).backgroundColor('#4FACFE')
// 竖向居中圆点
Column() {
Text('新版本 v2.3.1 发布上线') // 活动标题
Text('2 小时前') // 时间(灰色弱化)
}
}
圆点颜色从预定义的调色板中取(蓝、绿、粉、紫、橙),增强了视觉节奏感。
三个交互点
- 统计卡片点击 — 弹窗展示详情 + “查看详情”/"关闭"按钮
- 进度条颜色自动映射 — 无需点击,颜色随百分比自动变化
- 日期实时显示 — 页面加载时显示当天日期
六、完整代码
import { AppColors, BorderRadius, FontSize, Spacing } from '../common/Constants';
import { promptAction } from '@kit.ArkUI';
class StatItem {
label: string;
value: string;
icon: string;
gradientStart: string;
gradientEnd: string;
constructor(label: string, value: string, icon: string, gradientStart: string,
gradientEnd: string) {
this.label = label;
this.value = value;
this.icon = icon;
this.gradientStart = gradientStart;
this.gradientEnd = gradientEnd;
}
}
class ProgressItem {
title: string;
percent: number;
color: string;
updatedAt: string;
constructor(title: string, percent: number, color: string, updatedAt: string) {
this.title = title;
this.percent = percent;
this.color = color;
this.updatedAt = updatedAt;
}
}
class ActivityItem {
title: string;
time: string;
dotColor: string;
constructor(title: string, time: string, dotColor: string) {
this.title = title;
this.time = time;
this.dotColor = dotColor;
}
}
const STATS: StatItem[] = [
new StatItem('总收入', '¥128,930', '💰', '#4FACFE', '#00F2FE'),
new StatItem('活跃用户', '8,562', '👥', '#A18CD1', '#FBC2EB'),
new StatItem('订单数', '3,241', '📦', '#FA709A', '#FEE140'),
new StatItem('增长率', '+24.5%', '📈', '#43E97B', '#38F9D7'),
];
const PROJECTS: ProgressItem[] = [
new ProgressItem('App 3.0 重构', 75, '#4FACFE', '更新于 2 小时前'),
new ProgressItem('支付系统对接', 60, '#A18CD1', '更新于 1 天前'),
new ProgressItem('官网营销页', 92, '#43E97B', '更新于 3 小时前'),
new ProgressItem('用户画像系统', 45, '#FA709A', '更新于 6 小时前'),
new ProgressItem('自动化测试覆盖', 68, '#FFB347', '更新于 1 天前'),
];
const ACTIVITIES: ActivityItem[] = [
new ActivityItem('新版本 v2.3.1 发布上线', '2 小时前', '#4FACFE'),
new ActivityItem('服务器弹性扩容完成', '5 小时前', '#43E97B'),
new ActivityItem('用户反馈工单处理完毕', '昨天 18:30', '#FA709A'),
new ActivityItem('Q3 产品设计稿评审通过', '昨天 14:00', '#A18CD1'),
new ActivityItem('数据库慢查询优化上线', '2 天前', '#FFB347'),
];
@Entry
@Component
struct DashboardPage {
@State stats: StatItem[] = STATS;
@State projects: ProgressItem[] = PROJECTS;
@State activities: ActivityItem[] = ACTIVITIES;
build() {
Column() {
Column() {
Row() {
Column() {
Text('数据看板')
.fontSize(FontSize.HEADLINE)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
Text('Dashboard')
.fontSize(FontSize.CAPTION)
.fontColor('#FFFFFF88')
.margin({ top: 2 })
}
Blank()
Column() {
Text(this.getDateStr())
.fontSize(FontSize.CAPTION)
.fontColor('#FFFFFFAA')
}
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor('#FFFFFF22')
.borderRadius(9999)
}
.width('100%')
.margin({ bottom: Spacing.LG })
}
.width('100%')
.padding({ left: Spacing.XXL, right: Spacing.XXL, top: 44, bottom: Spacing.LG })
.linearGradient({
angle: 180,
colors: [['#1a1a2e', 0.0], ['#16213e', 0.5], ['#0f3460', 1.0]]
})
Scroll() {
Column() {
Row() {
ForEach(this.stats.slice(0, 2), (stat: StatItem) => {
Column() {
Text(stat.icon).fontSize(30).margin({ bottom: Spacing.SM })
Text(stat.value)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.margin({ bottom: 2 })
Text(stat.label)
.fontSize(FontSize.CAPTION)
.fontColor('#FFFFFF99')
}
.width('100%')
.padding({ top: Spacing.XL, bottom: Spacing.XL,
left: Spacing.LG, right: Spacing.LG })
.borderRadius(BorderRadius.LG)
.linearGradient({
angle: 135,
colors: [[stat.gradientStart, 0.0], [stat.gradientEnd, 1.0]]
})
.layoutWeight(1)
.margin({ right: 8 })
.onClick(() => {
promptAction.showDialog({
title: stat.label,
message: `${stat.label}:${stat.value}\n\n详细数据请查看报表中心`,
buttons: [{ text: '查看详情', color: stat.gradientStart },
{ text: '关闭', color: AppColors.TEXT_TERTIARY }]
});
})
})
}
.width('100%')
.margin({ bottom: 8 })
Row() {
ForEach(this.stats.slice(2, 4), (stat: StatItem) => {
Column() {
Text(stat.icon).fontSize(30).margin({ bottom: Spacing.SM })
Text(stat.value)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.margin({ bottom: 2 })
Text(stat.label)
.fontSize(FontSize.CAPTION)
.fontColor('#FFFFFF99')
}
.width('100%')
.padding({ top: Spacing.XL, bottom: Spacing.XL,
left: Spacing.LG, right: Spacing.LG })
.borderRadius(BorderRadius.LG)
.linearGradient({
angle: 135,
colors: [[stat.gradientStart, 0.0], [stat.gradientEnd, 1.0]]
})
.layoutWeight(1)
.margin({ right: 8 })
.onClick(() => {
promptAction.showDialog({
title: stat.label,
message: `${stat.label}:${stat.value}\n\n详细数据请查看报表中心`,
buttons: [{ text: '查看详情', color: stat.gradientStart },
{ text: '关闭', color: AppColors.TEXT_TERTIARY }]
});
})
})
}
.width('100%')
.margin({ bottom: Spacing.XL })
Text('项目进度')
.fontSize(FontSize.HEADLINE)
.fontWeight(FontWeight.Bold)
.fontColor(AppColors.TEXT_PRIMARY)
.width('100%')
.margin({ bottom: Spacing.MD })
Column() {
ForEach(this.projects, (proj: ProgressItem) => {
Column() {
Row() {
Text(proj.title)
.fontSize(FontSize.BODY)
.fontColor(AppColors.TEXT_PRIMARY)
.fontWeight(FontWeight.Medium)
Blank()
Text(`${proj.percent}%`)
.fontSize(FontSize.BODY)
.fontWeight(FontWeight.Bold)
.fontColor(proj.color)
}
.width('100%')
.margin({ bottom: 6 })
Progress({ value: proj.percent, total: 100,
type: ProgressType.Linear })
.width('100%')
.height(8)
.color(this.getProgressColor(proj.percent))
.backgroundColor('#F0F0F0')
Text(proj.updatedAt)
.fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_TERTIARY)
.width('100%')
.margin({ top: 4 })
}
.width('100%')
.padding({ top: Spacing.MD, bottom: Spacing.MD,
left: Spacing.LG, right: Spacing.LG })
.backgroundColor(Color.White)
.borderRadius(BorderRadius.SM)
.margin({ bottom: Spacing.SM })
})
}
.width('100%')
.margin({ bottom: Spacing.XL })
Text('最近动态')
.fontSize(FontSize.HEADLINE)
.fontWeight(FontWeight.Bold)
.fontColor(AppColors.TEXT_PRIMARY)
.width('100%')
.margin({ bottom: Spacing.MD })
Column() {
ForEach(this.activities, (act: ActivityItem, index: number) => {
Row() {
Row() {
Row()
.width(10).height(10)
.borderRadius(5)
.backgroundColor(act.dotColor)
}
.width(24).height('100%')
.justifyContent(FlexAlign.Center)
Column() {
Text(act.title)
.fontSize(FontSize.BODY)
.fontColor(AppColors.TEXT_PRIMARY)
.fontWeight(FontWeight.Medium)
Text(act.time)
.fontSize(FontSize.CAPTION)
.fontColor(AppColors.TEXT_TERTIARY)
.margin({ top: 2 })
}
.layoutWeight(1)
}
.width('100%')
.padding({ top: Spacing.MD, bottom: Spacing.MD,
left: Spacing.LG, right: Spacing.LG })
.backgroundColor(Color.White)
.borderRadius(BorderRadius.SM)
.margin({ bottom: 4 })
})
}
.width('100%')
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG,
top: Spacing.XL, bottom: Spacing.XXL })
}
.layoutWeight(1)
.scrollBar(BarState.Off)
.backgroundColor('#F2F3F7')
}
.width('100%')
.height('100%')
}
getProgressColor(percent: number): string {
if (percent >= 80) return '#43E97B';
if (percent >= 60) return '#4FACFE';
if (percent >= 40) return '#FFB347';
return '#FA709A';
}
getDateStr(): string {
const d = new Date();
return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-` +
`${d.getDate().toString().padStart(2, '0')}`;
}
}
七、常见面试题 / 踩坑点
7.1 LinearGradient 的 angle 和 direction 怎么选?
优先使用 angle 而不是 direction。angle 的值是标准的 CSS 渐变角度(0=右,90=下,135=对角线,180=左),更直观且跨平台一致。GradientDirection 枚举在不同版本的 ArkUI 中名称可能不同(Bottom、Top、Left、Right 是稳定值,对角线方向的枚举名各版本差异大)。
7.2 渐变卡片上的文字用什么颜色?
几乎总是白色。在彩色渐变背景上,白色文字通过不同的不透明度来建立层级:
- 主要信息(数值):纯白
#FFFFFF或 90%+ 不透明度 - 辅助信息(标签):
#FFFFFF99(60% 不透明度) - 弱化信息(时间、备注):
#FFFFFF66(40% 不透明度)
暗色文字在渐变背景上容易在某个颜色区域看不清。
7.3 Progress 的 height 设置多大合适?
- 列表中的进度条:4-6vp,不抢眼
- 卡片中的进度条:6-8vp,有存在感但不突兀
- 强调型进度条(如上传大文件):10-12vp,用户需要看清进度
低于 4vp 的进度条在手机上几乎看不清,高于 12vp 看起来像"粗条"而非"细线进度条"。
7.4 如何让页面看起来"贵"?
三个低成本技巧:
- 浅灰背景 + 白色卡片 — 灰底
#F2F3F7让白卡片浮起来,成本 0 - 渐变替代纯色 — 统计卡片、标题栏用
linearGradient对角线渐变,立刻"贵"一档 - 半透明白色文字 — 渐变背景上用
#FFFFFF99做辅助文字,显得"高级"
7.5 slice 方法在 ForEach 中能用吗?
Array.slice() 返回新数组,在 ForEach 中直接使用没问题,不需要提前存 @State:
ForEach(this.stats.slice(0, 2), (stat: StatItem) => { ... })
但注意,slice 每次 render 都会创建新数组,数据量大时建议拆成两个 @State 数组。
八、总结
ArkUI 的视觉效果不是"加个颜色就行"——它需要理解渐变的角度、颜色的语义、圆角的层级、卡片的分层。本文用数据看板这一场景,展示了三个核心视觉技巧:
1. LinearGradient 是"质感"的来源。 纯色 → 渐变的改变,是 ArkUI 页面从"能用"到"好看"的分水岭。对角线渐变(135°)给卡片注入光影立体感,多色渐变给页面背景注入层次感。
2. Progress 是"数据故事"的载体。 不要把所有进度条都涂成蓝色。绿/蓝/橙/红的色彩映射让用户不看数字就能感知状态——这是仪表盘区别于普通列表的核心能力。
3. 卡片设计靠"分层"建立信息架构。 灰底 + 白卡 + 圆角 + 统一内边距——这四个元素的组合让信息从扁平变得有层次。不需要阴影(ArkUI 阴影性能开销大),灰底白卡足够让卡片"浮起来"。
数据看板 / Dashboard 特别适合以下场景:
- 企业管理后台(收入、用户、订单概览)
- 项目管理工具(迭代进度、任务完成率)
- 个人数据面板(学习进度、健康数据、财务概览)
- SaaS 产品首页(关键指标一览)
掌握了渐变、进度条和卡片设计这三个"视觉杠杆",ArkUI 页面的品质感可以从"内部工具"提升到"商用产品"的水平。
更多推荐




所有评论(0)