HarmonyOS 引导页完全实现指南:从路由选型到取色器动画

本文基于 HarmonyOS ArkTS 开发范式,以「喵屿」宠物陪伴应用的引导页模块为实例,从需求分析、导航选型、动画实现、取色器应用到交互细节,系统讲解引导页的完整实现方案。


目录

  1. 为什么需要引导页
  2. 引导页跳转:Router 与 Navigation
  3. 引导页动画实现
  4. colorPicker.getMainColorSync() 的使用
  5. 引导页其他实现细节
  6. 总结

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),可做登录校验、权限控制
  • 支持 replacePathpopToNamepopToIndex 等丰富的栈操作
  • @Provide/@Consume 状态管理深度集成,跨页面通信更自然
  • 支持自定义转场动画(pageTransition

Navigation 的缺点

  • 学习曲线较 Router 更陡,需理解 NavPathStack、NavDestination、route_map 等概念
  • 必须作为 @Entry 组件的顶层容器,灵活性受限
  • 页面跳转需要 Builder 函数导出,增加模板代码

2.3 本项目双轨制的设计考量

维度 引导页阶段(Router) 主框架阶段(Navigation)
页面范围 仅 FirstPage → Index 一次性跳转 20+ 子页面复杂导航
导航需求 单向、不可逆的替换跳转 多向、可逆的栈导航
生命周期 跳转后引导页销毁 主框架常驻,子页面压栈/出栈
传参需求 无需传参 频繁跨页面传参

选择在引导页到主框架之间使用 router.replaceUrl 而不是 Navigation 的原因:

  1. 隔离性。引导页是“一次性页面”,跳转后应立即销毁且不可回溯。replaceUrl 语义精确匹配这一需求——它替换当前页面并销毁之。如果在 Index 的 Navigation 内部通过 pushPathByName 跳转引导页,引导页将留在导航栈中,破坏“一次性”的语义。

  2. 启动体量Navigation 组件需要初始化完整的导航栈、路由表和子页面上下文。若在 EntryAbility 中直接加载带 Navigation 的页面,启动负载更高。而 FirstPage 作为轻量页面先行渲染,用户看到引导内容的延迟更短。

  3. 架构清晰。导航体系切换的时机(引导页 → 主框架)恰好是用户行为的分界线。“进入前”使用简单的 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.ArkGraphics2DeffectKit 模块提供的智能取色器,能从 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) {
    // 取色失败时使用默认背景色,不影响页面渲染
  }
}

数据处理流程:

  1. 资源读取:通过 resourceManager.getMediaContent()Uint8Array 形式获取 app.media.background 的原始字节数据。
  2. 图像解码:使用 image.createImageSource(buffer) 创建图像源,再调用 createPixelMap() 解码为 PixelMap
  3. 取色器创建:通过 effectKit.createColorPicker(pixelMap, callback) 异步创建取色器实例。
  4. 主色提取:调用 colorPicker.getMainColorSync() 同步获取 Color 对象。
  5. 颜色转换Color 包含 alpharedgreenblue 四个 0-255 范围的属性,拼接为十六进制颜色字符串。生产环境建议使用 .toString(16).padStart(2, '0') 补齐两位。
  6. 动画过渡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,失败不影响核心流程。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐