HarmonyOS 沉浸导航栏变色案例:从取色到全屏动态主题
效果
一、案例概述
本案例实现了一个全屏动态变色导航栏——当用户滑动轮播 Banner 时,系统通过 @ohos.effectKit 自动提取当前图片的主色调,并将该颜色同步应用到:
- 顶部状态栏背景(实时变色)
- 底部导航栏背景(毛玻璃质感 + 变色)
- Banner 下方渐变过渡条(主色→白色渐变)
- 切换指示器颜色(随主色动态变化)
实现图片内容与界面色调的深度融合,打造沉浸式视觉体验。
效果链路
Banner 切换 → effectKit 取色 → ThemeModel 更新 →
├─ 顶部状态栏:backgroundColor(dominantColor)
├─ 渐变过渡条:dominantColor → white
├─ 底部导航栏:navBarColor + 毛玻璃
└─ 指示器/图标:dominantColor
技术栈
| 技术 | 说明 |
|---|---|
| ArkTS + ArkUI | 声明式 UI 框架 |
| @ohos.effectKit | ColorPicker 图像主色调提取 |
| State Management V2 | @ObservedV2 + @Trace + @Local |
| Scroll + Grid | 滚动 + 网格布局组合 |
| backgroundBlurStyle | 毛玻璃效果 |
二、项目结构
entry/src/main/ets/
├── model/
│ └── ThemeModel.ets # V2 可观察颜色主题模型
├── constants/
│ └── StyleConstants.ets # 全局样式常量
├── pages/
│ └── Index.ets # 主页面(@ComponentV2 + @Entry)
└── entryability/
└── EntryAbility.ets # 应用入口,全屏 + 避让区域
三、核心实现详解
3.1 颜色主题模型(ThemeModel)
@ObservedV2
export class ColorThemeModel {
@Trace dominantColor: string = '#7C4DFF'; // 提取的主色调(全色)
@Trace isDark: boolean = true; // 颜色明暗判断
@Trace navBarColor: string = '#D07C4DFF'; // 导航栏背景(90%透明度)
@Trace ambientGlowColor: string = '#267C4DFF'; // 环境光晕(35%透明度)
updateFromColor(color: string): void {
this.dominantColor = color;
this.isDark = this.calcBrightness(color) < 160;
this.navBarColor = ColorThemeModel.hexWithAlpha(color, 0.90);
this.ambientGlowColor = ColorThemeModel.hexWithAlpha(color, 0.35);
}
/** 亮度计算:ITU-R BT.601 加权公式 */
private calcBrightness(hex: string): number {
const r = parseInt(hex.substring(1, 3), 16);
const g = parseInt(hex.substring(3, 5), 16);
const b = parseInt(hex.substring(5, 7), 16);
return (r * 299 + g * 587 + b * 114) / 1000;
}
/** 为十六进制颜色添加 Alpha 透明度 */
static hexWithAlpha(hex: string, alpha: number): string {
const a = Math.round(alpha * 255).toString(16).padStart(2, '0');
return '#' + a + hex.substring(1);
}
}
关键参数:
navBarColor使用 90% 透明度,在白色背景下既能清晰显色又保留毛玻璃透感;ambientGlowColor使用 35% 透明度,用于环境光晕。
3.2 effectKit 取色流水线
private async extractColor(resId: number): Promise<void> {
// 1. 获取 ResourceManager
const ctx = this.getUIContext().getHostContext() as Context;
const resMgr: resourceManager.ResourceManager = ctx.resourceManager;
// 2. 图片二进制 → ImageSource → PixelMap
const fileData: Uint8Array = resMgr.getMediaContentSync(resId);
const imgSrc: image.ImageSource = image.createImageSource(fileData.buffer);
const pixelMap: image.PixelMap = await imgSrc.createPixelMap();
// 3. 创建 ColorPicker → 提取主色
effectKit.createColorPicker(pixelMap, (_err: BusinessError, colorPicker) => {
const color = colorPicker.getMainColorSync();
// 4. Color → #AARRGGBB
const hexColor = '#' +
color.alpha.toString(16).padStart(2, '0') +
color.red.toString(16).padStart(2, '0') +
color.green.toString(16).padStart(2, '0') +
color.blue.toString(16).padStart(2, '0');
// 5. 更新模型 → 自动触发 UI 刷新
this.themeModel.updateFromColor(hexColor);
});
}
⚠️ 重要陷阱:toString(16) 后必须使用 padStart(2, '0'),否则小于 16 的分量值(如 alpha=10)会输出单字符 "a" 而非 "0a",导致颜色格式错误。
3.3 白色背景布局架构
与常见的暗色沉浸方案不同,本案例采用白色背景,让变色效果更加清晰直观:
build() {
Column() {
// 顶部状态栏 - 随主色变色
Row()
.height(this.topRectHeight + 4)
.width('100%')
.backgroundColor(this.themeModel.dominantColor) // ← 核心变色点
Scroll() {
Column() {
// Banner 轮播
Swiper() { ... }
// 渐变过渡条:主色 → 白色(取色效果直观展示)
Row()
.height(40).width('100%')
.linearGradient({
colors: [[this.themeModel.dominantColor, 0.0], ['#FFFFFF', 1.0]]
})
// 标题区 + 内容网格
Row() { Text('灵感精选')... }
Grid() { ... }
}
}
.layoutWeight(1)
// 底部导航栏 - 毛玻璃变色
Row() { this.buildTabItem(...)... }
.backgroundColor(this.themeModel.navBarColor)
.backgroundBlurStyle(BlurStyle.Thin)
}
.backgroundColor('#FFFFFF') // 白色基底
}
布局演进说明:
- 最初采用
Stack双层叠加 + 暗色背景 + 环境光晕,但深色基底压制了变色效果 - 最终改为纯
Column布局 + 白色背景,利用「白色画布」让颜色变化一目了然 - 状态栏和导航栏使用
dominantColor/navBarColor直接显色,而非半透明叠加
3.4 顶部状态栏变色
// 状态栏高度从 AppStorage 读取,避免全屏模式下被系统栏遮挡
aboutToAppear(): void {
this.topRectHeight = AppStorage.get<number>('topRectHeight') ?? 0;
this.bottomRectHeight = AppStorage.get<number>('bottomRectHeight') ?? 0;
this.extractColor(this.banners[0].id);
}
在 EntryAbility 中通过 setWindowLayoutFullScreen(true) 启用全屏,并获取避让区域:
windowClass.setWindowLayoutFullScreen(true);
const uiContext = windowClass.getUIContext();
const topRectHeight = uiContext.px2vp(avoidArea.topRect.height);
AppStorage.setOrCreate('topRectHeight', topRectHeight);
// 注册动态监听
windowClass.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('topRectHeight', uiContext.px2vp(data.area.topRect.height));
}
});
3.5 底部导航栏毛玻璃变色
// 底部导航栏
Row() {
this.buildTabItem(0, $r('app.media.home'), '首页')
this.buildTabItem(1, $r('app.media.comments_selected'), '发现')
this.buildTabItem(2, $r('app.media.mine_selected'), '我的')
}
.width('100%')
.height(64)
.backgroundColor(this.themeModel.navBarColor) // 90%透明度主色
.backgroundBlurStyle(BlurStyle.Thin) // 毛玻璃
.padding({ bottom: this.bottomRectHeight + 4 }) // 避开系统导航条
Tab 项图标和文字自适应:
@Builder
buildTabItem(index: number, icon: Resource, label: string) {
Column() {
Image(icon)
.width(24).height(24)
.fillColor(this.currentTab === index
? this.themeModel.dominantColor // 选中色 = 主色
: '#99000000') // 未选中 = 半透明黑
Text(label)
.fontSize(11)
.fontColor(this.currentTab === index
? Color.Black
: '#66000000')
}
}
3.6 内容卡片风格
白色卡片 + 阴影,适配白色背景:
GridItem() {
Column() {
Image(item.img).width('100%').height(90).objectFit(ImageFit.Contain)
Column() {
Text(item.title).fontColor('#1A1A2E')
Text(item.desc).fontColor('#999999')
}.padding(10)
}
.height(150).borderRadius(16)
.backgroundColor(Color.White) // 纯白背景
.shadow({ // 投影增加层次感
radius: 8, color: '#1A000000',
offsetX: 0, offsetY: 2
})
}
四、V2 状态管理要点
| V1 | V2 | 本案例用途 |
|---|---|---|
| @State | @Local | 主题模型引用、当前索引 |
| @Observed | @ObservedV2 | ColorThemeModel 类 |
| @Track | @Trace | 4 个颜色属性(dominantColor/isDark/navBarColor/ambientGlowColor) |
| @Builder | @Builder | buildTabItem 导航项构建 |
| @Component | @ComponentV2 | 主页面结构体 |
五、编译常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
Namespace 'window' has no exported member 'AvoidAreaEvent' | 类型不存在 | 移除类型注解,让编译器自动推断 (data) => |
Identifier 'DOMAIN' has already been declared | 文件写入工具追加导致重复代码 | 检查并删除重复的 import 和 const DOMAIN |
"import" statements after other statements are not allowed | import 位置错误 | 确保所有 import 在文件最顶部 |
| 状态栏避空失效,Banner 紧贴顶部 | 未从 AppStorage 读取高度 | aboutToAppear 中调用 AppStorage.get() |
| 导航栏变色不明显 | 深色背景压制颜色 | 改用白色背景 #FFFFFF + 显色参数 |
六、运行与调试
6.1 前提条件
- DevEco Studio 6.1+ / HarmonyOS NEXT SDK API 23+
- 真机或模拟器运行(建议真机以获得完整毛玻璃效果)
6.2 调试建议
- 在
extractColor中添加hilog输出 hexColor 值,验证取色是否正确 - 使用 DevEco Studio 的 Inspector 工具检查状态栏和导航栏的背景色
- 切换不同色系的 Banner 图片,观察颜色变化范围
七、扩展思路
| 扩展方向 | 实现思路 |
|---|---|
| 颜色渐变动画 | 使用 animateTo 包裹 updateFromColor() 调用 |
| 暗黑模式 | 根据 isDark 自动切换页面背景色 |
| 多图片源 | 扩展为网络图片 + 缓存 PixelMap |
| 音乐可视化 | 配合 AudioKit 提取专辑封面色调 |
| 动态壁纸 | 将主色扩展到整个桌面级主题 |

422

被折叠的 条评论
为什么被折叠?



