HarmonyOS 引导页完全实现指南:从路由选型到取色器动画
本文详细介绍了在HarmonyOS ArkTS中实现引导页的完整方案。主要内容包括: 引导页的作用分析:降低认知负荷、传递品牌温度、决策分流 导航系统设计:采用双轨制方案,引导页使用Router.replaceUrl跳转,主框架使用Navigation导航 动画实现:包括Swiper页面切换动画、组件过渡动画和交互动画 取色器应用:使用colorPicker.getMainColorSync()提
HarmonyOS 引导页完全实现指南:从路由选型到取色器动画
本文基于 HarmonyOS ArkTS 开发范式,以「喵屿」宠物陪伴应用的引导页模块为实例,从需求分析、导航选型、动画实现、取色器应用到交互细节,系统讲解引导页的完整实现方案。
目录
1. 为什么需要引导页
引导页是用户首次打开应用时的“第一印象”。对于「喵屿」这类功能丰富的宠物陪伴应用而言,引导页承担三重职责:
降低认知负荷。应用涵盖疫苗管理、驱虫记录、库存追踪、费用统计、智能问答、萌宠互动等多个模块。若直接进入主界面,新用户面对密集的功能入口容易迷失。引导页将核心价值提炼为简洁的文案,逐条展示,帮助用户在 10 秒内建立心智模型。
传递品牌温度。「喵屿」的品牌调性是“暖心陪伴”,引导页通过品牌形象展示、渐变色背景、交互引导等设计语言,在功能引导的同时完成情感传递。
决策分流。通过 Preference 持久化存储 firstPage 标记,引导页只在首次安装时展示。用户点击“开始探索”后标记为已读,后续启动直接进入主框架,避免重复打扰。
// FirstPage.ets — aboutToAppear()
let needShowFirstPage =
PreferenceUtil.getPreferenceByNameSync(BaseConstants.DEFAULT_NAME, BaseConstants.KEY_FIRST_PAGE, true)
if (!needShowFirstPage) {
this.router() // 非首次启动,直接跳转到应用主框架
}
// SecondView.ets — “开始探索”按钮回调
PreferenceUtil.putPreferenceByName(BaseConstants.DEFAULT_NAME, BaseConstants.KEY_FIRST_PAGE, false)
引导页静态图片:
引导页动态图片:
2. 引导页跳转:Router 与 Navigation
「喵屿」的导航体系采用了双轨制:引导页使用 router.replaceUrl() 跳转到 Index 页面,而 Index 之后的所有页面则使用 Navigation + NavPathStack 进行导航。本节分别介绍两种方案的特性,并解释这样设计的原因。
2.1 Router API:页面替换跳转
router 是 HarmonyOS 传统的页面路由模块,导入自 @kit.ArkUI。核心方法包括 pushUrl(压栈跳转)、replaceUrl(替换跳转)、back(返回)等。
在 FirstPage 中,用户完成引导后调用 replaceUrl 替换到 Index:
import { router } from '@kit.ArkUI';
router = () => {
this.getUIContext().getRouter().replaceUrl({
url: 'pages/Index'
}, router.RouterMode.Standard, (err) => {
if (err) {
console.error(`Invoke replaceUrl failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info('Invoke replaceUrl succeeded.');
})
}
replaceUrl 的特点是销毁当前页,替换为新页面。用户从 Index 页返回时不会回到引导页,而是退出应用——这正是引导页所需的行为。
Router 的优点:
- API 简单直观,学习成本低
- 页面栈模型清晰,适合线性流程
replaceUrl天然适合一次性页面(引导页、登录页、启动页)
Router 的缺点:
- 页面必须在
main_pages.json中注册,不支持动态路由 - 跨页面传参需通过
params字段,缺乏类型安全 - 无法实现复杂的嵌套导航(如 Tab + Stack 混合)
- 页面栈上限 32 层,超出需手动
clear() - 不支持路由拦截、自定义转场动画等高级特性
2.2 Navigation API:声明式导航框架
Navigation 是 HarmonyOS 官方推荐的新一代导航组件,其核心是 NavPathStack(导航栈)+ NavDestination(导航目标页)的组合。
在 Index.ets 中,Navigation 作为应用主框架容器:
@Entry
@Component
struct Index {
@Provide('pageInfo') pathStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.pathStack) {
}
.onAppear(() => { this.pathStack.pushPathByName('MainPage', '') })
.hideNavBar(true)
}
}
所有子页面通过 @Consume('pageInfo') pathStack: NavPathStack 获取导航栈引用,调用 pushPathByName 进行页面跳转:
this.pathStack.pushPathByName('TimeLine', '')
this.pathStack.pushPathByName('VaccineManagement', '')
this.pathStack.pushPathByName('Settings', undefined)
Navigation 的优点:
- 支持路由表注册(
route_map.json),实现声明式路由配置 - 支持
NavDestination嵌套,实现复杂的页面层级 - 支持路由拦截(
onIntercept),可做登录校验、权限控制 - 支持
replacePath、popToName、popToIndex等丰富的栈操作 - 与
@Provide/@Consume状态管理深度集成,跨页面通信更自然 - 支持自定义转场动画(
pageTransition)
Navigation 的缺点:
- 学习曲线较 Router 更陡,需理解 NavPathStack、NavDestination、route_map 等概念
- 必须作为
@Entry组件的顶层容器,灵活性受限 - 页面跳转需要 Builder 函数导出,增加模板代码
2.3 本项目双轨制的设计考量
| 维度 | 引导页阶段(Router) | 主框架阶段(Navigation) |
|---|---|---|
| 页面范围 | 仅 FirstPage → Index 一次性跳转 | 20+ 子页面复杂导航 |
| 导航需求 | 单向、不可逆的替换跳转 | 多向、可逆的栈导航 |
| 生命周期 | 跳转后引导页销毁 | 主框架常驻,子页面压栈/出栈 |
| 传参需求 | 无需传参 | 频繁跨页面传参 |
选择在引导页到主框架之间使用 router.replaceUrl 而不是 Navigation 的原因:
-
隔离性。引导页是“一次性页面”,跳转后应立即销毁且不可回溯。
replaceUrl语义精确匹配这一需求——它替换当前页面并销毁之。如果在 Index 的 Navigation 内部通过pushPathByName跳转引导页,引导页将留在导航栈中,破坏“一次性”的语义。 -
启动体量。
Navigation组件需要初始化完整的导航栈、路由表和子页面上下文。若在 EntryAbility 中直接加载带 Navigation 的页面,启动负载更高。而 FirstPage 作为轻量页面先行渲染,用户看到引导内容的延迟更短。 -
架构清晰。导航体系切换的时机(引导页 → 主框架)恰好是用户行为的分界线。“进入前”使用简单的 Router,“进入后”使用功能完备的 Navigation,职责边界清晰,便于维护。
注意事项:Router 和 Navigation 不应在同一个页面中混用。Router 无法直接跳转到 NavDestination 页面,需先跳转到承载 Navigation 的根页面(本项目中的 Index)再进行内部导航。
3. 引导页动画实现
「喵屿」引导页包含两个 Swiper 子页面,动画体系分为三层:Swiper 页面切换动画、组件入场过渡动画(TransitionEffect)、交互动画(猫咪跳动、按钮动态效果)。
3.1 Swiper 容器动画
Swiper 是引导页的顶层容器,承载 FirstView 和 SecondView 两个子页面:
Swiper(this.controller) {
FirstView({ controller: this.controller, curIndex: this.curIndex })
SecondView({ onRouter: this.router, curIndex: this.curIndex })
}
.loop(false)
.autoPlay(false)
.displayCount(1)
.duration(800)
.curve(Curve.Rhythm)
.indicator(false)
.itemSpace(30)
.margin({ top: 15 })
.width("100%")
.animation({
duration: 800,
curve: Curve.Friction,
iterations: 1,
playMode: PlayMode.Normal
})
.onChange(index => {
this.curIndex = index
})
关键属性说明:
loop(false):禁止循环轮播,确保引导页只展示一轮autoPlay(false):禁止自动播放,由用户主动滑动或点击按钮翻页displayCount(1):每屏只显示一个子页面duration(800):翻页动画持续 800 毫秒,配合Curve.Rhythm曲线实现舒缓的翻页节奏indicator(false):隐藏默认指示器,引导页使用自定义 UI 元素引导翻页animation:为 Swiper 自身绑定动画配置,Curve.Friction模拟物理惯性效果onChange:监听页面切换,更新curIndex状态驱动子页面逻辑
3.2 TransitionEffect 交错入场动画
TransitionEffect 是 HarmonyOS 从 API 10 开始提供的组件出现/消失转场 API。它采用函数链式组合的方式构建转场效果:move() 定义方向、opacity() 定义透明度、combine() 组合叠加、animation() 定义时间曲线。
FirstView 中所有可视元素从屏幕底部滑入并伴随透明度渐变,每个元素通过递增的 delay 形成交错入场效果:
const baseDelay = 300
const delay = 100
const opacity = 0
// 品牌图片 — delay: 400ms
Image($r("app.media.background"))
.size({ width: 150, height: 150 })
.objectFit(ImageFit.Cover)
.borderRadius(120)
.transition(
TransitionEffect.move(TransitionEdge.BOTTOM)
.animation({
duration: 1000,
delay: baseDelay + delay,
curve: Curve.Ease
})
.combine(TransitionEffect.opacity(opacity))
)
// 标题 “喵屿” — delay: 500ms
Text("「喵屿」")
.fontSize(25)
.fontColor($r("[resource].color.orange"))
.transition(
TransitionEffect.move(TransitionEdge.BOTTOM)
.animation({
duration: 1000,
delay: baseDelay + delay * 2,
curve: Curve.Ease
})
.combine(TransitionEffect.opacity(opacity))
)
// HDC 勋章 — delay: 600ms
Text(' HDC 2026 鸿蒙星光大道推荐应用 ')
.transition(
TransitionEffect.move(TransitionEdge.BOTTOM)
.animation({
duration: 1000,
delay: baseDelay + delay * 3,
curve: Curve.Ease
})
.combine(TransitionEffect.opacity(opacity))
)
// 欢迎文案(ForEach 动态生成)— delay: 700ms ~ 1200ms
ForEach(textArray, (item: string, index) => {
Text(item)
.fontSize(15)
.transition(
TransitionEffect.move(TransitionEdge.BOTTOM)
.animation({
duration: 1000,
delay: baseDelay + delay * (index + 4),
curve: Curve.Ease
})
.combine(TransitionEffect.opacity(opacity))
)
})
// “Next” 按钮 — delay: 3000ms(最晚出现)
Text("Next")
.transition(
TransitionEffect.move(TransitionEdge.BOTTOM)
.animation({
duration: 1000,
delay: 3000,
curve: Curve.Ease
})
.combine(TransitionEffect.opacity(opacity))
)
交错入场的视觉效果:品牌图片率先出现(400ms),随后标题浮现(500ms),勋章标签跟上(600ms),接着 6 条欢迎文案依次亮相(700ms ~ 1200ms),最后 “Next” 按钮在 3000ms 后优雅登场。整个过程形成清晰的视觉引导流,用户的注意力自上而下逐层聚焦。
3.3 TranslateOptions 与 TransitionEdge 的关系
在 TransitionEffect 中,move(TransitionEdge.BOTTOM) 是一种语义化的平移效果封装,它等价于:
// move(TransitionEdge.BOTTOM) 的内部等价行为
TransitionEffect.translate({ y: '100%' })
两者的区别在于:
| 特性 | move(TransitionEdge) |
translate(TranslateOptions) |
|---|---|---|
| 参数语义 | 屏幕边缘方向(TOP/BOTTOM/START/END) | 精确的三维位移值(x/y/z) |
| 位移量 | 自动计算为组件自身的尺寸 | 需手动指定像素或百分比值 |
| 适用场景 | 从屏幕外滑入/滑出的标准转场 | 需要精确控制位移量的自定义动画 |
在引导页实现中,选择 move(TransitionEdge.BOTTOM) 而非 translate 的原因:move 自动适配不同屏幕尺寸和组件高度,无需手动计算位移量,且语义更清晰——“元素从底部滑入”直接表达设计意图。
3.4 背景渐变动画
FirstPage 的 Column 背景使用了 linearGradient 渐变,通过 animateTo 驱动 bgColor 属性变化实现背景色平滑过渡:
Column() {
Swiper(this.controller) { /* ... */ }
}
.width('100%').height('100%')
.backgroundColor($r('[resource].color.mainBackground'))
.linearGradient({
direction: GradientDirection.Bottom,
colors: [[this.bgColor, 0.0], [$r('[resource].color.mainBackground'), 0.5]]
})
linearGradient 从顶部(由 bgColor 决定)渐变到底部(mainBackground 固有色),过渡点设在 50% 处。当 bgColor 通过 animateTo 变化时,上半部分的渐变颜色随之平滑过渡。这为下一节要介绍的取色器应用做了铺垫——bgColor 的值来源于品牌图片的主色调。
4. colorPicker.getMainColorSync() 的使用
4.1 API 概述
ColorPicker 是 HarmonyOS @kit.ArkGraphics2D 中 effectKit 模块提供的智能取色器,能从 PixelMap 图像中提取代表性颜色。其核心方法对比:
| 方法 | 返回方式 | 算法原理 |
|---|---|---|
getMainColorSync() |
同步返回 Color |
综合混合所有颜色的分布与饱和度,输出“最具代表性”的主色 |
getMainColor() |
Promise 异步返回 Color |
同上,异步版本 |
getLargestProportionColor() |
异步返回 Color |
纯频率统计,返回像素占比最高的颜色 |
getMainColorSync() 适用于已知 PixelMap 已就绪、需要同步读取的 UI 线程场景。
4.2 在本项目中的应用
FirstPage.ets 中定义了 getBgColor() 方法,从品牌图片 app.media.background 中提取主色调,用作页面顶部渐变色的起始色,实现背景色与品牌视觉自动协调的效果:
import { effectKit } from '@kit.ArkGraphics2D';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
async getBgColor() {
try {
const context = getContext(this);
// 获取 resourceManager 资源管理器
const resourceMgr: resourceManager.ResourceManager = context.resourceManager;
// 读取媒体资源的原始字节数据
const fileData: Uint8Array = await resourceMgr.getMediaContent($r("app.media.background"));
// 获取图片的 ArrayBuffer
const buffer = fileData.buffer as ArrayBuffer;
// 创建 ImageSource
const imageSource: image.ImageSource = image.createImageSource(buffer);
// 解码为 PixelMap
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
// 通过回调方式创建 ColorPicker
effectKit.createColorPicker(pixelMap, (err, colorPicker) => {
// 同步读取图像主色
let color = colorPicker.getMainColorSync();
// 通过属性动画平滑过渡背景色
animateTo({ duration: 500, curve: Curve.Linear, iterations: 1 }, () => {
// 将 Color 转换为十六进制颜色代码
this.bgColor = "#" +
color.alpha.toString(16) +
color.red.toString(16) +
color.green.toString(16) +
color.blue.toString(16);
})
})
} catch (e) {
// 取色失败时使用默认背景色,不影响页面渲染
}
}
数据处理流程:
- 资源读取:通过
resourceManager.getMediaContent()以Uint8Array形式获取app.media.background的原始字节数据。 - 图像解码:使用
image.createImageSource(buffer)创建图像源,再调用createPixelMap()解码为PixelMap。 - 取色器创建:通过
effectKit.createColorPicker(pixelMap, callback)异步创建取色器实例。 - 主色提取:调用
colorPicker.getMainColorSync()同步获取Color对象。 - 颜色转换:
Color包含alpha、red、green、blue四个 0-255 范围的属性,拼接为十六进制颜色字符串。生产环境建议使用.toString(16).padStart(2, '0')补齐两位。 - 动画过渡:
animateTo驱动bgColor在 500ms 内线性过渡到新颜色。
4.3 注意事项
- 取色精度:
getMainColorSync()在多色混合场景下可能产生“调和色”(如蓝 + 白 + 黄混合出浅绿)。如果取色结果不符合预期,可改用getLargestProportionColor()获取像素占比最高的颜色。 - 容错处理:
getBgColor()整体包裹在try-catch中。取色失败时bgColor保持初始值$r('[resource].color.gray_3'),页面仍正常渲染——取色器是锦上添花的增强,而非阻塞性功能。
5. 引导页其他实现细节
5.1 沉浸式窗口模式
引导页通过 WindowModel 设置沉浸式窗口,隐藏系统状态栏和导航栏的视觉干扰:
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
let context = getContext() as common.UIAbilityContext;
const windowStage: window.WindowStage | undefined = context.windowStage;
this.windowModel.setWindowStage(windowStage);
this.windowModel.setMainWindowImmersive(true);
同时获取状态栏高度和底部导航栏高度,用于 padding 适配,避免内容被系统 UI 遮挡:
this.windowModel.getBottomAvoidHeight((bottomHeight) => {
this.bottomHeight = px2vp(bottomHeight);
})
this.windowModel.getStatusBarHeight((statusBarHeight) => {
this.statusBarHeight = px2vp(statusBarHeight);
})
// 在 build() 中应用
Column() { /* ... */ }
.padding({ top: this.statusBarHeight, bottom: this.bottomHeight })
5.2 SwiperController 页面切换控制
SwiperController 提供翻页能力。在 FirstView 中,“Next” 按钮通过 controller.showNext() 触发翻页:
controller = new SwiperController()
@State canSkip: boolean = false
// 3 秒后启用按钮,防止用户过早跳过
timeoutId_1 = setTimeout(() => {
this.canSkip = true
}, 3000)
// Next 按钮
Text("Next")
.onClick(() => {
if (this.canSkip) {
this.controller.showNext()
}
})
这个 3 秒延迟设计是一个微妙的体验优化:如果按钮立即可点击,用户可能在动画尚未完成时就跳转到第二页,导致入场动画被截断。3 秒的等待期确保了交错的 TransitionEffect 动画全部完成后,用户才能进入下一页。
5.3 Preference 持久化与引导状态管理
引导页的状态通过 Preference 持久化,控制整个生命周期:
import PreferenceUtil from 'resource/src/main/ets/utils/PreferenceUtil';
import { BaseConstants } from 'resource';
// 读取:首次启动默认为 true(需要显示引导页)
let needShow = PreferenceUtil.getPreferenceByNameSync(
BaseConstants.DEFAULT_NAME, // 'catIsland'
BaseConstants.KEY_FIRST_PAGE, // 'firstPage'
true // 默认值
)
// 写入:用户完成引导后标记为 false
PreferenceUtil.putPreferenceByName(
BaseConstants.DEFAULT_NAME,
BaseConstants.KEY_FIRST_PAGE,
false
)
5.4 页面入口配置
FirstPage 在页面路由表和 EntryAbility 中的配置是引导页能被正确加载的基础:
main_pages.json:
{
"src": [
"pages/FirstPage",
"pages/Index"
]
}
EntryAbility.onWindowStageCreate():
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/FirstPage', (err) => {
AppStorage.setOrCreate('uiContext', windowStage.getMainWindowSync().getUIContext());
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
loadContent('pages/FirstPage', ...) 将 FirstPage 设为应用冷启动的渲染入口。FirstPage 内部的 aboutToAppear 再根据 KEY_FIRST_PAGE 标记决定是展示引导流程还是直接跳转到 Index。
6. 总结
「喵屿」引导页的实现是一个完整的 HarmonyOS 技术实践案例,涵盖了从页面路由、动画系统、图像处理到交互反馈的多个技术维度:
| 技术点 | 方案选型 | 关键 API |
|---|---|---|
| 页面容器 | Swiper 滑块容器 | Swiper + SwiperController |
| 首次跳转 | Router 替换跳转 | router.replaceUrl() |
| 主框架导航 | Navigation 声明式导航 | Navigation + NavPathStack.pushPathByName() |
| 入场动画 | 交错 TransitionEffect | TransitionEffect.move().combine().animation() |
| 背景渐变 | 线性渐变 + animateTo | linearGradient() + animateTo() |
| 主色提取 | effectKit 取色器 | effectKit.createColorPicker() + getMainColorSync() |
| 状态持久化 | Preference 键值存储 | PreferenceUtil.getPreferenceByNameSync() |
| 窗口适配 | 沉浸式模式 | windowStage + setMainWindowImmersive() |
引导页的代码量虽小,但技术密度高。它处于用户从“安装完成”到“开始使用”的关键转化节点,每一处动画时序、交互反馈和导航决策都直接影响用户对应用品质的第一判断。本文展示的实现方案遵循以下原则:
- 先体验、后框架:引导页使用轻量 Router,完成后再进入 Navigation 主框架。
- 动画错落有致:TransitionEffect 交错入场 + 物理曲线交互动画,视觉层次分明。
- 容错优先:取色器等增强功能包裹 try-catch,失败不影响核心流程。
更多推荐



所有评论(0)