Navigation与NavDestination
前言
在开发面试通的过程中我突然想做一些丝滑的动画,但我发现我好像从来没用过Navigation
和NavDestination
,所以我决定好好研究一下这两个组件的用法。
接下来我就会和大家一起去学习这两个组件的使用以及动画效果的是实现。
组件用途
这里还是先附上两者的官方文档:
Navigation API15
这两者都是用于进行页面跳转的组件,Navigation
是用于定义导航图,而NavDestination
则是用于定义具体的页面。
两者自带一多能力,可以依据当前设备进行导航方式的选择。
这是在直板机场景下采用单栏显示模式,子页面的内容会直接替换掉主页面的内容。
而当我们处于折叠屏展开或是平板场景下则会采用分栏展示模式,子页面的内容会被分栏展示在主页面的右侧。
两者的区别如下图所示。
Navigation
常用于首页的根组件,两者结合常用于类似设置的场景,可以进行多级跳转,或在平板上进行分栏展示子页面内容,同时支持系统默认动画或是自定义转场动画。其效果可以参考以下视频:
直板机:
平板:
当然我们如果想让平板也拥有和手机一样的效果,在首页的内容目录中点击想要浏览的页面后直接跳转至对应页面,而不是说依旧在左侧保留首页的同时在右侧分栏展示所选内容,那我们就需要将Navigation
的mode
属性设置为NavigationMode.Stack
,这样就可以实现和手机一样的效果了。
因为默认的效果是NavigationMode.Auto
,也就是自动依据直板机、折叠屏、平板等设备进行适配,所以平板的默认效果是分栏展示的。
与Router
的区别
对于这两者的区别主要体现在以下几点上:
Navigation
支持更丰富的动效Navigation
内置一次开发多端部署能力,无需额外适配Navigation
更灵活的栈操作- 更加轻量化的跳转方式,性能更加优秀
Router
不具备路由拦截能力,而Navigation
则具备Navigation
没有路由数量限制,而Router
则有32个的限制
就凭这几点,Navigation
的优势就非常明显了。
而两者最本质的区别在于Router
是通过切换页面来进行路由,所以它存在多个页面,但是Navigation
是通过切换NavDestination
子组件来进行路由的,其全称都只有一个页面。
这张表是对Navigation
和Router
的对比,可以很明显的看出两者的区别。
Navigation
的跳转逻辑
对于Navigation
组件来说它不能直接想是router
一样直接跳转,而是需要配合一下几个辅助条件才能实现跳转:
- 子页面根组件
NavDestination
- 路由表
RouterMap
- 页面栈
NavPathStack
这几者的关系如下图所示:
Navigation
作为路由导航的根视图容器,而NavDestination
则是用来显示Navigation
的内容区域,一般作为跳转的目标页面。
RouterMap
路由表RouterMap
是用于定义页面跳转的规则,它负责存储路径和对应视图组件的映射关系,我们可以将他理解为一个小型的数据库,我们要去哪里都需要通过在RouterMap
中查找路径来实现跳转。
而在跳转时只需要通过NavPathStack
提供的路由方式传入需要跳转的页面配置名称即可。
然后对于RouterMap
的配置项及其含义我们可以参考下表:
配置项 | 含义 |
---|---|
name |
页面名称,用于在NavPathStack 中进行跳转 |
pageSourceFile |
跳转目标页面在当前包内的路径,是相对于src 文件夹的相对路径 |
buildFunction |
目标页入口函数名称,用于构建目标页面的视图组件,必须以@Builder进行修饰 |
data |
自定义字段。可以通过配置项读取接口getConfigInRouteMap获取 |
RouterMap
的配置文件
为了存储RouterMap
的配置项,我们需要在resources/base/profile
文件夹下创建一个配置文件,并将其命名为router_map.json
。
然后我们在module.json5
中添加如下配置,用以绑定routerMap
的配置文件:
1 | { |
然后我们就可以在router_map.json
中添加我们的配置项了,如下所示:
1 | { |
这样我们就完成了对RouterMap
的配置。
NavPathStack
页面栈NavPathStack
是用于存储当前页面路径的栈,它负责进行页面跳转。
当页面跳转触发时页面栈就会通过路由表RouterMap
来查找路径对应的页面,然后将其压入栈中,并跳转到该路径对应的页面。
在Navigation
组件中,我们通过NavPathStack
的push
方法进行页面跳转,通过pop
方法进行页面返回。
NavPathStack
的常见操作
push
:将路径压入栈中,并跳转到该路径对应的页面pop
:将路径从栈中弹出,并返回到上一个路径对应的页面replace
:将路径替换为栈顶路径,并跳转到该路径对应的页面
push
方法
首先对于push
方法来说,我们需要通过一个NavPathInfo
对象来传入路径信息:
1 | export const NAV_PATH_STUCK = 'navPathStuck' |
上面是首页的代码,由于多个页面需要共享一个页面栈来进行跳转到操作所以我们使用全局变量将页面栈存入AppStorageV2
或是通过跨代际传递的装饰器@Provider
来共享页面栈。
同时我们还利用了param
字段来进行参数的传递。
下面我们来编写一下跳转的目标页面Second
:
1 | import { NAV_PATH_STUCK } from "./Index" |
首先第二页包含了一个入口构造Builder
函数,一个NavDestination
组件作为跳转的目标页。随后我们在该组件的aboutToAppear
生命周期函数中获取了NavPathStack
中的参数,并打印出来。
要注意:getIndexByName('Second')[0]
这段这么写是因为当前的NavPathStack
中可能存在多个名为Second
的路径,所以我们需要通过getIndexByName
方法获取到该路径在栈中的索引,其返回值是一个数组而非一个数字。
虽然这段代码中我们使用的是aboutToAppear
生命周期函数来取获取的数据,但对于NavDestination
组件来说,官方更推荐我们使用下文会提到的onReady
生命周期函数来获取数据。
1 | import { NAV_PATH_STUCK } from "./Index" |
然后这段代码的效果如下视频所示:
但很显然现在的效果并不对,我们在多次点击之后跳转的页面很显然不是我们想要的指定页面,它产生了多个同名的组件实例。
为了解决这个问题,我首先尝试了采用替换的方式来进行跳转,这样每次都会是我们所指定的子页面。
但在测试后发现这样的效果很显然与我们的预期不符,每次点击依旧会有明显的闪烁。就像下面这个视频一样。
我尝试的第二种方法则是获取当前页面栈中已经有的全部页面名,判断是否存在目标页面。
如果存在则将目标页面已经存在的那个实例置于栈顶进行显示。
1 | Button('PushPath') |
效果如下:
这就与我们的预期效果一致了。
pop
方法
pop
是出栈函数,我们将当前栈顶,也就是正在显示的子页面出栈,并携带参数返回到上一个页面。
1 | export const NAV_PATH_STUCK = 'navPathStuck' |
在第一个页面中我们将上一个事例中的NavPathInfo
对象查出来单独编写,这样方便管理,同时我们定义接口来规范pop
方法返回的参数类型。
而在第二个页面中我们通过NavPathStack
的pop
方法来返回数据。
其效果如下:
很显然这里还是存在bug的,回传的数据并没有传递到主页。
我们添加一些日志来进行debug:
日志很清晰的告诉了我们,返回的结果并不在"info"
字段里,而是在"result"
字段里。
enm……这件事再次警告我们要认真读文档!!!
1 | onPop:(popInfo)=>{ |
再次测试:
这次数据就正确返回了。
多子页面页面栈管理
单开一个这个小标题是接续上文提到的push
方法导致的多页面实例问题。
添加限制符
这是最基础的防止反复压栈的处理方式,通过设置状态变量或全局变量来限制push
方法的调用,当第一次调用push
方法时将状态变量或全局变量设置为true
,当再次调用push
方法时判断状态变量或全局变量是否为true
,如果为true
则不调用push
方法。
随后在pop
方法中再次将状态变量或全局变量设置为false
,这样就可以保证push
方法只调用一次。
1 | hasPushedSecond: boolean = false // 新增标志位 |
当然这是一种比较原始且通用的方式,属于是在各个技术栈的开发中都能想到并应用的低成本方式。
但很显然它也有它的缺点。
这种方式的本质仅仅是对用户的操作进行了限制,并不会去对操作的结果是否达到了目标进行判断,也就是说用户有可能绕过限制,也有可能因为系统卡顿内存不足导致子页面没有完成构建,但push
操作的限制符已经锁死,这就会导致子页面无法正常显示。
所以这种方式也是不建议使用的。
检测页面栈中的页面
这种方式是通过NavPathStack
的getAllPathName
方法来获取页面栈中全部的页面,并利用数组内置函数includes
来去判断是否存在目标页面,如果存在则不调用push
方法,存在的话就调用moveIndexToTop
方法将已经存在的页面实例从下方抽出放置到栈顶进行显示。
这种方式就抓住了核心问题所在,也就是当前页面栈是否已经创建过目标页面的实例,而不是像上一种方式一样仅仅是从能触及核心问题的操作的角度出发进行限制。
Navigation
组件的生命周期
对于该组件的生命周期与普通组件稍有差异。
因为,Navigation
组件的路由页面实际是一个个NavDestination
容器组件,与Router
中的一个个新的@Entry
组件不同,其切换的成本更低,速度更快也支持更加丰富的转场能力。
但要注意的是NavDestination
容器组件外层还会有一层自定义组件,就像上面我的自定义组件Second
包裹着一个NavDestination
容器组件。
因此!!!我们可以将Navigation
组件的生命周期理解为 自定义组件的生命周期+NavDestination容器组件的生命周期
。
由此我们可以推导出如下图所示的生命周期函数图:
对于各个生命周期函数的触发时机如下图所示:
而这其中我们重点要关注onReady
这个生命周期函数,因为这是Navigation
独有的生命周期函数,它触发于即将构建子组件之前,也就是说它触发时子组件还没有构建。
可以在这里面做一些数据的准备工作,像是获取传参,拿到组件的上下文组件。
Navigation
组件的转场效果
默认转场动画
上面这张图很好的展示了Navigation
组件的转场效果简介,对于非Dialog
模式的页面我们都可以直接使用系统默认的转场动画。
默认转场动画的使用
使用系统默认的转场动画方式非常简单,我们只需要在页面栈NavPathStack
中添加页面时,在第二个参数传入一个true即可开启默认的转场动画。
自定义转场动画
对于自定义动画其实现方式要稍微复杂一些,首先是要通过在Navigation
组件的customNavContentTransition
事件去获取即将触发动画的来去两个页面的页面信息,再从CustomTransitionFW
动画框架工具类中查询注册的动画信息。
不过要注意我们的动画效果依旧是通过transelate
属性去实现的所以我们仍旧需要为组件添加该属性。
利用BuilderMap
对RouterMap
进行替换
昨天在编辑RouterMap
配置文件时,感觉手动填写相对路径并且进行相关配置的填写来实现路由表的构建还是有些麻烦,而且容易错,于是我再次阅读文档加上阅读了一些开源软件的代码,发现其实可以用一个BuilderMap
来对RouterMap
进行替换,从而简化路由表的构建。
BuilderMap
的构建方式如下:
- 创建路由页面路径的常量,所有的页面路由都要用常量进行控制防止出现没有代码提示导致的输入错误
- 创建一个
BuilderMap
的Builder
然后设定一个字符串类型的形参进行当前栈顶页面的名称获取 - 判断当前的栈顶页面值,并跳转至对应的子页面组件
1 |
|
然后将各个页面的入口builder
以及之前在module.json5
中配置的routerMap
字段删除,并将routerMap
文件删除。不过这里要注意,由于我们没有了配置文件,我们需要将页面栈的初始化提前,提前至页面构建之前,否则会在子页面中获取页面栈对象时报错页面栈未初始化。
1 | onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { |
我们可以看到我们成功的实现了简洁且有代码提示的路由表构建,并且可以很方便的添加新的页面。
自定义动画的简单应用
这里介绍一种简单的丝滑小动画,不用向上面一样的准备工具类啥的,我们只需要在每个子页面添加一个动画属性即可。
1 | export const animateParam: AnimateParam = { duration: 1500, curve: curves.springMotion(0.6, 0.7) }; |
嗯!看着还是很舒服的。
工具栏切换页面
对于工具栏,我在学习一多之前进行了尝试但是效果和我想的不一样,所以我决定先去学一多,所以咱们可以移步一多的博客了。