鸿蒙开发之组件导航 (Navigation)

815 阅读19分钟

一、概述

Navigation 是路由容器组件,可作为首页根容器,有单栏(Stack)、分栏(Split)和自适应(Auto)三种显示模式,适用于模块内和跨模块路由切换,可一次开发多端部署,通过组件级路由能力实现自然流畅转场体验,提供多种标题栏样式呈现更好标题和内容联动效果,且能在不同尺寸设备上自适应显示大小并自动切换分栏展示效果。主要包含导航页(NavBar)和子页(NavDestination)。 Navigation组件的基本使用参考官方文档

image.png

二、显示模式设置

(一)自适应模式

默认模式,当页面宽度大于等于一定阈值(API version 9 及以前:520vp,API version 10 及以后:600vp)时采用分栏模式,反之采用单栏模式。

复制

Navigation() {
  //...
}
.mode(NavigationMode.Auto)

(二)单页面模式

单页面布局示意图

image.png

将 mode 属性设置为 NavigationMode.Stack。

Navigation() {
  //...
}
.mode(NavigationMode.Stack)

(三)分栏模式

将 mode 属性设置为 NavigationMode.Split。

image.png

@Entry
@Component
struct NavigationExample {
  //...
  build() {
    Column() {
      Navigation(this.pageInfos) {
        //...
      }
     .title("主标题")
     .mode(NavigationMode.Split)
      //...
    }
  }
}

三、标题栏模式设置

通过 titleMode 属性设置标题栏模式。

(一)Mini 模式

普通型标题栏,用于一级页面不需要突出标题的场景。

Navigation() {
  //...
}
.titleMode(NavigationTitleMode.Mini)

效果示例:

image.png

(二)Full 模式

强调型标题栏,用于一级页面需要突出标题的场景。

Navigation() {
  //...
}
.titleMode(NavigationTitleMode.Full)

效果示例:

image.png

四、设置菜单栏

位于 Navigation 组件右上角,可通过 menus 属性设置,支持 <NavigationMenuItem>和CustomBuilder两种参数类型。竖屏最多显示 3 个图标,横屏最多显示 5 个图标,多余图标放入自动生成的更多图标。

// 定义一个名为TooTmp的导航菜单项,用于特定的导航操作
import { promptAction } from '@kit.ArkUI'
//基础班示例,用的话就根据实际设计图换下图片以及点击逻辑
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {
//轻提示
  promptAction.showToast({
                    message: `点击了TooTmp菜单项`,
                    bottom:350
                  })
}}


@Entry
@Component
struct NavBarExample {
  build() {

    Navigation(){
      Column(){
      Text(` NavBarExample页面`)

      Text(`内容1`)
        .width('100%')
        .height(200)
        .textAlign(TextAlign.Center)
        .backgroundColor(Color.Pink)

      Text(`内容2`)
        .width('100%')
        .height(200)
        .textAlign(TextAlign.Center)
        .backgroundColor(Color.Gray)
        Text(`内容3`)
          .width('100%')
          .height(200)
          .textAlign(TextAlign.Center)
          .backgroundColor(Color.Orange)
      }
    }.title("我是主标题")

    .mode(NavigationMode.Auto)
    //.titleMode(NavigationTitleMode.Mini)
    .titleMode(NavigationTitleMode.Full)
    .menus([TooTmp,
      TooTmp,
      TooTmp])
  }
}

效果示例:

PixPin_2024-09-26_11-39-32.gif

五、设置工具栏

位于 Navigation 组件底部,可通过 toolbarConfiguration 属性设置。

let TooTmp1: ToolbarItem = {'value': "func", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {

promptAction.showToast({
  message: `点击了TooTmp1工具栏`,
  bottom:350
})
}}
let TooBar: ToolbarItem[] = [TooTmp1,TooTmp1,TooTmp1]
Navigation() {
  //...
}
.toolbarConfiguration(TooBar)

效果示例:这是由于示例图片原因所以不太明显,我增一个点击事件方便判断

recording.gif

六、路由操作

基于页面栈 NavPathStack 提供方法进行,每个 Navigation 需创建并传入一个 NavPathStack 对象管理页面。

(一)页面跳转

前期的准备,Navigation实现路由跳转时需提前配置路由表 (跨包动态路由跳转)

  1. 新建 Builder 入口函数 和 NavDestination 组件
@Builder
function NavBarExampleBuilder() {
// 1.2 子页结构需要用 NavDestination 包裹
NavDestination() {
  // 页面结构封装成组件
  NavBarExample()
}.title('NavBarExample页面one')

}


@Component
struct NavBarExample {
@Consume pageStack: NavPathStack
build() {
    Column(){
    Text(`内容1`)
      .width('100%') 
      .height(200)
      .textAlign(TextAlign.Center)
      .backgroundColor(Color.Pink)

    Text(`内容2`)
      .width('100%')
      .height(200)
      .textAlign(TextAlign.Center)
      .backgroundColor(Color.Gray)
      Text(`内容3`)
        .width('100%')
        .height(200)
        .textAlign(TextAlign.Center)
        .backgroundColor(Color.Orange)

      Button('点我返回')
      .onClick(()=>{
        // 1.3 返回到上一页
        this.pageStack.pop()
      })
    }    .height('100%')

}
}

2. 添加完路由配置文件地址后,需要在工程 resources/base/profile 中创建 route_map.json 文件。添加如下配置信息:(名字信息要一致,新增页面还需要追加路由信息)

{
 "routerMap": [
   {
     "name": "NavBarExample",
     "pageSourceFile": "src/main/ets/pages/NavBarExample.ets",
     "buildFunction": "NavBarExampleBuilder",
     "data": {
       "description": "this is NavBarExample"
     }
   }
 ]
}

3. 在跳转目标模块的配置文件 module.json5 添加路由表配置:

{
 "module": {
   "name": "home",
   "type": "shared",
   "description": "$string:shared_desc",
   "deviceTypes": [
     "phone",
     "tablet",
     "2in1"
   ],
   "deliveryWithInstall": true,
   "pages": "$profile:main_pages",
   "routerMap": "$profile:route_map"
 }
}
  1. 普通跳转:通过页面 name 跳转并可携带 param。
.onClick(() => {
  // 3. 通过路由栈跳转页面
  this.pageStack.pushPath({ name: 'NavBarExample',  param:new Object({id:'123456'}) })
  // this.pageStack.pushPathByName('xxx', 'xxx Param')
})

跳转目标页面接收

Button('点我获取参数')
.onClick(()=>{
  // 获取 SearchView 页面的参数,注意返回的是数组类型
  const params = this.pageStack.getParamByName('NavBarExample') as PageParams[]
  promptAction.showToast({
                    message: `${params[0].id}`,
                    bottom:350
                  })
})

效果演示

recording.gif

  1. 带返回回调的跳转:跳转时添加 onPop 回调,在页面出栈时获取返回信息并处理。

  2. 带错误码的跳转:跳转结束触发异步回调返回错误码信息。

(二)页面返回

  1. 返回到上一页:this.pageStack.pop()
  2. 返回到上一个特定页面:this.pageStack.popToName("PageOne")
  3. 返回到指定索引页面:this.pageStack.popToIndex(1)
  4. 返回到根首页(清除栈中所有页面):this.pageStack.clear()

(三)页面替换

页面替换功能允许在导航过程中用新的页面替换当前页面,为应用的流程控制提供了灵活的手段。

可以使用this.pageStack.replacePath({ name: 'NewPage', param: { /* 新页面参数 */ } })来进行页面替换操作。这样可以在不影响页面栈的整体结构的情况下,更新当前显示的页面内容。

例如,当用户在某个流程中需要更新特定页面的信息时,可以使用页面替换功能,将旧页面替换为包含最新数据的新页面,为用户提供无缝的交互体验。

(四)页面删除

  1. 删除栈中特定 name 的所有页面:

    • 使用this.pageStack.removeByName("PageOne")可以快速清理页面栈中特定名称的所有页面。这在需要根据特定条件清理页面栈时非常有用。
    • 例如,当某个业务流程结束后,可能需要删除与该流程相关的特定页面,以释放资源并保持页面栈的整洁。
  2. 删除指定索引的页面:

    • this.pageStack.removeByIndexes([1,3,5])允许根据索引删除特定的页面。这为开发者提供了更精细的页面栈管理方式。

    • 例如,在复杂的导航场景中,可以根据特定的逻辑条件删除特定索引的页面,以优化页面栈的结构和性能。

(五)参数获取

  1. 获取栈中所有页面 name 集合:

    • this.pageStack.getAllPathName()可以方便地获取页面栈中所有页面的名称集合。这对于了解应用的导航历史和当前页面栈的状态非常有帮助。
    • 例如,可以在调试或分析应用的导航流程时使用这个方法,快速查看页面栈中的页面名称列表。
  2. 获取索引为特定值的页面参数:

    • this.pageStack.getParamByIndex(1)可以根据索引获取特定页面的参数。这在需要根据页面在栈中的位置获取特定页面的参数时非常有用。
    • 例如,当需要根据页面的顺序进行特定的操作时,可以使用这个方法获取相应页面的参数,以实现更加精准的业务逻辑处理。
  3. 获取特定 name 页面的参数:

    • this.pageStack.getParamByName("PageOne")允许根据页面名称获取特定页面的参数。这在需要根据页面名称进行参数获取时非常方便。
    • 例如,当需要根据特定页面的名称获取其参数,以进行特定的业务处理或状态恢复时,可以使用这个方法。
  4. 获取特定 name 页面的索引集合:

    • this.pageStack.getIndexByName("PageOne")可以获取特定名称页面在页面栈中的索引集合。这对于确定特定页面在栈中的位置非常有帮助。

    • 例如,在需要根据页面名称查找其在页面栈中的位置时,可以使用这个方法获取相应的索引集合,以便进行进一步的操作。

(六)路由拦截

路由拦截功能为应用的导航流程提供了强大的控制手段。通过 setInterception 方法设置 Navigation 页面跳转拦截回调,可以在页面跳转的不同阶段进行干预。

传入的 NavigationInterception 对象包含三个回调函数:

  • willShow(页面跳转前回调,可操作栈):在页面跳转前触发,可以对页面栈进行操作,例如添加、删除或修改页面。这为开发者提供了在页面跳转前进行预处理的机会。

  • didShow(页面跳转后回调):在页面跳转后触发,可以进行一些后续的处理,如更新全局状态、发送通知等。

  • modeChange(Navigation 单双栏显示状态变更时触发):当 Navigation 的单双栏显示状态发生变化时触发,可以根据不同的显示状态进行相应的调整。

例如,可以在 willShow 回调中检查用户的权限,以决定是否允许页面跳转。在 didShow 回调中,可以根据新页面的加载情况进行一些额外的操作,如显示加载进度条或进行数据初始化。而在 modeChange 回调中,可以根据不同的显示状态调整页面的布局和样式,以提供更好的用户体验。

七、子页面(NavDestination)

(一)页面显示类型

  1. 标准类型

    • 作为默认类型,在NavDestinationMode.STANDARD模式下,子页面的呈现较为常规。其生命周期紧密跟随在NavPathStack页面栈中的位置变化而动态调整。这意味着当页面在栈中的位置发生改变时,例如被推入栈顶或从栈中弹出,子页面的显示状态和可交互性也会相应地发生变化。开发人员可以利用这一特性,根据页面在栈中的位置来定制不同的交互逻辑和显示效果。
    • 例如,当一个子页面被推入栈顶成为当前显示页面时,可以触发特定的动画效果或加载特定的数据,以提升用户体验。而当页面从栈中弹出时,可以进行数据清理或保存用户操作的状态,以便下次进入该页面时能够恢复到上次离开时的状态。
  2. 弹窗类型

    • 通过将mode属性设置为NavDestinationMode.DIALOG,可以使整个NavDestination呈现为弹窗样式。在这种模式下,子页面默认以透明显示,为用户提供一种轻量级的交互方式。弹窗类型的子页面在显示和消失时,不会对下层标准类型的NavDestination产生影响,两者可以同时显示,从而实现了更加灵活的页面布局和交互设计。

    • 例如,在一个应用中,可以使用弹窗类型的子页面来显示重要的通知、提示信息或进行快速的用户交互,而不会干扰到应用的主要内容区域。同时,标准类型的子页面可以继续在后台运行,保持应用的整体流程不受影响。

(二)页面生命周期

  1. 自定义组件生命周期

    • aboutToAppear:在创建自定义组件后,执行其build()函数之前触发。开发人员可以在这个阶段进行一些初始化操作,例如设置状态变量或加载初始数据。由于更改在后续执行build()函数中生效,因此可以确保组件在显示之前已经准备好所需的数据和状态。
    • onWillAppear:在NavDestination创建后,挂载到组件树之前执行。在这个阶段更改状态变量会在当前帧显示生效,适用于需要立即响应的情况,例如根据特定条件动态调整页面的显示内容。
    • onAppear:通用生命周期事件,在NavDestination组件挂载到组件树时执行。可以在此处进行一些与页面显示相关的操作,例如启动动画效果、发送页面显示的通知等。
    • aboutToDisappear:自定义组件析构销毁之前执行。需要注意的是,在这个方法中不允许改变状态变量,以避免出现不可预测的行为。
    • onDisappear:通用生命周期事件,在NavDestination组件从组件树上卸载销毁时执行。可以在此处进行一些清理操作,例如释放资源、保存用户数据等。
  2. 通用组件生命周期

    • OnAppearOnDisappear是组件的通用生命周期事件,与其他组件的生命周期事件类似,可以在这些阶段进行一些通用的操作,例如更新全局状态、发送通知等。
  3. NavDestination 独有生命周期

    • onWillShow:在NavDestination组件布局显示之前执行,此时页面不可见。这个阶段可以用于进行一些预加载操作,例如加载数据或启动动画的准备工作。

    • onShown:在NavDestination组件布局显示之后执行,此时页面已完成布局。可以在此处进行一些与页面显示相关的操作,例如调整布局、显示动画效果等。

    • onWillHide:在NavDestination组件触发隐藏之前执行。与onWillShow相对应,这个阶段可以用于进行一些隐藏前的准备工作,例如保存用户数据或停止动画效果。

    • onHidden:在NavDestination组件触发隐藏后执行。可以在此处进行一些清理操作,例如释放资源、停止后台任务等。

    • onWillDisappear:在NavDestination组件即将销毁之前执行,如果有转场动画,会在动画前触发。可以在此处进行一些最后的清理操作,例如保存用户数据、发送页面销毁的通知等。

(三)页面监听和查询

  1. 页面信息查询

    • 自定义组件提供的queryNavDestinationInfo方法是一种方便的方式来查询当前所属页面的信息。通过调用这个方法,开发人员可以获取到NavDestinationInfo对象,其中包含了关于当前页面的各种信息,例如页面的名称、参数、状态等。如果查询不到相关信息,则返回undefined
    • 例如,在一个复杂的应用中,可能有多个子页面具有相似的布局和功能,但根据不同的页面信息进行不同的处理。通过使用queryNavDestinationInfo方法,可以在自定义组件中动态地调整显示内容和行为,以适应不同的页面需求。
  2. 页面状态监听

    • 通过observer.on('navDestinationUpdate')提供的注册接口,开发人员可以注册NavDestination生命周期变化的监听。这样,当NavDestination的生命周期发生变化时,注册的回调函数将被触发,从而可以及时响应页面状态的改变。

    • 同时,还可以注册页面切换的状态回调,能在页面发生路由切换的时候拿到对应的页面信息NavDestinationSwitchInfo。并且提供了UIAbilityContextUIContext不同范围的监听,开发人员可以根据具体需求选择合适的监听范围。

    • 例如,在一个多页面应用中,可以使用页面状态监听来跟踪用户的导航路径,记录用户的操作历史,或者在页面切换时进行一些特定的动画效果或数据处理。

八、页面转场

(一)关闭转场

  1. 全局关闭

    • 通过NavPathStackdisableAnimation方法可以在当前Navigation中关闭所有转场动画。这在一些特定的场景下非常有用,例如需要快速切换页面而不希望有动画效果的干扰,或者为了提高性能而减少动画的开销。
    • aboutToAppear()方法中调用this.pageStack.disableAnimation(true)可以在组件创建时就关闭转场动画。开发人员可以根据应用的需求,在适当的时候开启或关闭全局转场动画,以实现更加灵活的用户体验。
  2. 单次关闭

    • NavPathStack中提供的PushPopReplace等接口中可以设置animated参数,默认为true表示有转场动画。如果需要单次关闭转场动画,可以将这个参数置为false,不影响下次转场动画。

    • 例如,在一些特定的操作中,如快速返回上一页或进行紧急的页面切换时,可以使用单次关闭转场动画来提高响应速度。同时,保留了下次操作时的转场动画可能性,以保持应用的整体视觉效果。

(二)自定义转场

  1. 构建自定义转场动画工具类

    • 构建一个自定义转场动画工具类CustomNavigationUtils,通过一个Map管理各个页面自定义动画对象CustomTransition。页面在创建的时候将自己的自定义转场动画对象注册进去,销毁的时候解注册。这样可以实现对不同页面的转场动画进行独立管理,提高代码的可维护性和灵活性。
    • 例如,可以根据不同的页面类型或业务需求,创建不同的自定义转场动画对象,并在工具类中进行统一管理。这样,在进行页面切换时,可以根据具体情况选择合适的转场动画,为用户提供更加丰富的视觉体验。
  2. 实现转场协议对象

    • 实现一个转场协议对象NavigationAnimatedTransition,其中timeout属性表示转场结束的超时时间,默认为 1000ms。transition属性为自定义的转场动画方法,开发者要在这里实现自己的转场动画逻辑,系统会在转场开始时调用该方法。onTransitionEnd为转场结束时的回调,可以在这个回调中进行一些清理操作或触发其他的业务逻辑。
    • 例如,可以使用动画库或自定义的动画函数来实现转场动画,根据页面的进入和退出方向、起始和结束状态等因素,设计出独特的转场效果。同时,通过设置合适的超时时间和回调函数,可以确保转场动画的流畅性和稳定性。
  3. 调用 customNavContentTransition 方法返回转场协议对象

    • 调用customNavContentTransition方法,返回实现的转场协议对象。如果返回undefined,则使用系统默认转场。这样可以在需要自定义转场动画的时候,通过实现转场协议对象并返回,来覆盖系统默认的转场效果。而在不需要自定义转场动画的情况下,直接返回undefined,使用系统提供的默认转场动画,保持简洁和高效。

    • 例如,在一些特定的页面切换场景中,可以根据页面的内容和风格,选择使用自定义转场动画来增强用户体验。而在其他普通的页面切换中,使用系统默认转场动画可以减少开发工作量,提高开发效率。

(三)共享元素转场

  1. 实现方式

    • NavDestination之间切换时,可以通过geometryTransition实现共享元素转场。配置了共享元素转场的页面同时需要关闭系统默认的转场动画,以确保共享元素转场的效果能够正确呈现。
    • 例如,在一个图片浏览应用中,可以使用共享元素转场来实现从一个图片页面切换到另一个图片页面时,图片的平滑过渡效果。通过为需要实现共享元素转场的组件添加geometryTransition属性,并设置相同的id参数,可以确保在页面切换时,系统能够正确识别和处理共享元素。
  2. 具体配置

    • 为需要实现共享元素转场的组件添加geometryTransition属性,id参数必须在两个NavDestination之间保持一致。例如:

    • 起始页配置共享元素id

NavDestination() {
  Column() {
    //...
    Image($r('app.media.startIcon'))
     .geometryTransition('sharedId')
     .width(100)
     .height(100)
  }
}
.title('FromPage')
  • 目的页配置共享元素id
NavDestination() {
  Column() {
    //...
    Image($r('app.media.startIcon'))
     .geometryTransition('sharedId')
     .width(200)
     .height(200)
  }
}
.title('ToPage')
  • 将页面路由的操作,放到animateTo动画闭包中,配置对应的动画参数以及关闭系统默认的转场。例如:
NavDestination() {
  Column() {
    Button('跳转目的页')
     .width('80%')
     .height(40)
     .margin(20)
     .onClick(() => {
        this.getUIContext()?.animateTo({ duration: 1000 }, () => {
          this.pageStack.pushPath({ name: 'ToPage' }, false)
        })
      })
  }
}
.title('FromPage')

九、跨包动态路由

(一)系统路由表

  1. 配置文件设置

    • 从 API version 12 开始,Navigation支持使用系统路由表的方式进行动态路由。各业务模块(HSP/HAR)中需要独立配置router_map.json文件,在触发路由跳转时,应用只需要通过NavPathStack提供的路由方法,传入需要路由的页面配置名称,此时系统会自动完成路由模块的动态加载、页面组件构建,并完成路由跳转,从而实现了开发层面的模块解耦。

    • 在跳转目标模块的配置文件module.json5添加路由表配置:

{
  "module": {
    "routerMap": "$profile:route_map"
  }
}
  • 添加完路由配置文件地址后,需要在工程resources/base/profile中创建route_map.json文件。添加如下配置信息:
{
  "routerMap": [
    {
      "name": "PageOne",
      "pageSourceFile": "src/main/ets/pages/PageOne.ets",
      "buildFunction": "PageOneBuilder",
      "data": {
        "description": "this is PageOne"
      }
    }
  ]
}
  • 配置说明如下:

    • name:跳转页面名称。

    • pageSourceFile:跳转目标页在包内的路径,相对src目录的相对路径。

    • buildFunction:跳转目标页的入口函数名称,必须以@Builder修饰。

    • data:应用自定义字段。可以通过配置项读取接口getConfigInRouteMap获取。

  1. 跳转页面入口函数设置

    • 在跳转目标页面中,需要配置入口Builder函数,函数名称需要和router_map.json配置文件中的buildFunction保持一致,否则在编译时会报错。

    • 例如:

// 跳转页面入口函数
@Builder
export function PageOneBuilder() {
  PageOne()
}

@Component
struct PageOne {
  pathStack: NavPathStack = new NavPathStack()

  build() {
    NavDestination() {
    }
   .title('PageOne')
   .onReady((context: NavDestinationContext) => {
       this.pathStack = context.pathStack
    })
  }
}

3. 页面跳转

*   通过`pushPathByName`等路由接口进行页面跳转。(注意:此时`Navigation`中可以不用配置`navDestination`属性)。

*   例如:
@Entry
@Component
struct Index {
  pageStack : NavPathStack = new NavPathStack();

  build() {
    Navigation(this.pageStack){
    }.onAppear(() => {
       this.pageStack.pushPathByName("PageOne", null, false);
    })
   .hideNavBar(true)
  }
}

(二)自定义路由表

  1. 实现方案
loading···