★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公眾號:山青詠芝(MaoistLearning)
➤部落格園地址:為敢技術(https://www.cnblogs.com/strengthen/ )
➤GitHub地址:https://github.com/strengthen
➤原文地址:https://www.cnblogs.com/strengthen/p/18476020
➤如果連結不是為敢技術的部落格園地址,則可能是爬取作者的文章。
➤原文已修改更新!強烈建議點選原文地址閱讀!支援作者!支援原創!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
Navigation是路由容器元件,一般作為首頁的根容器,包括單欄(Stack)、分欄(Split)和自適應(Auto)三種顯示模式。Navigation元件適用於模組內和跨模組的路由切換,一次開發,多端部署場景。透過元件級路由能力實現更加自然流暢的轉場體驗,並提供多種標題欄樣式來呈現更好的標題和內容聯動效果。在不同尺寸的裝置上,Navigation元件能夠自適應顯示大小,自動切換分欄展示效果。
Navigation元件主要包含導航頁(NavBar)和子頁(NavDestination)。導航頁由標題欄(Titlebar,包含選單欄menu)、內容區(Navigation子元件)和工具欄(Toolbar)組成,其中導航頁可以透過hideNavBar屬性進行隱藏,導航頁不存在頁面棧中,導航頁和子頁,以及子頁之間可以透過路由操作進行切換。
在API Version 9上,需要配合NavRouter元件實現頁面路由,從API Version 10開始,推薦使用NavPathStack實現頁面路由。
設定頁面顯示模式
Navigation元件透過mode屬性設定頁面的顯示模式。
-
自適應模式
Navigation元件預設為自適應模式,此時mode屬性為NavigationMode.Auto。自適應模式下,當頁面寬度大於等於一定閾值( API version 9及以前:520vp,API version 10及以後:600vp )時,Navigation元件採用分欄模式,反之採用單欄模式。
Navigation() { // ... } .mode(NavigationMode.Auto)
-
單頁面模式
圖1 單頁面佈局示意圖
將mode屬性設定為NavigationMode.Stack,Navigation元件即可設定為單頁面顯示模式。
Navigation() { // ... } .mode(NavigationMode.Stack)
-
分欄模式
圖2 分欄佈局示意圖
將mode屬性設定為NavigationMode.Split,Navigation元件即可設定為分欄顯示模式。
@Entry @Component struct NavigationExample { @State TooTmp: ToolbarItem = {'value': "func", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}} @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack() private arr: number[] = [1, 2, 3]; @Builder PageMap(name: string) { if (name === "NavDestinationTitle1") { pageOneTmp() } else if (name === "NavDestinationTitle2") { pageTwoTmp() } else if (name === "NavDestinationTitle3") { pageThreeTmp() } } build() { Column() { Navigation(this.pageInfos) { TextInput({ placeholder: 'search...' }) .width("90%") .height(40) .backgroundColor('#FFFFFF') List({ space: 12 }) { ForEach(this.arr, (item:number) => { ListItem() { Text("NavRouter" + item) .width("100%") .height(72) .backgroundColor('#FFFFFF') .borderRadius(24) .fontSize(16) .fontWeight(500) .textAlign(TextAlign.Center) .onClick(()=>{ this.pageInfos.pushPath({ name: "NavDestinationTitle" + item}) }) } }, (item:number) => item.toString()) } .width("90%") .margin({ top: 12 }) } .title("主標題") .mode(NavigationMode.Split) .navDestination(this.PageMap) .menus([ {value: "", icon: "./image/ic_public_search.svg", action: ()=> {}}, {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}, {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}, {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}}, {value: "", icon: "./image/ic_public_add.svg", action: ()=> {}} ]) .toolbarConfiguration([this.TooTmp, this.TooTmp, this.TooTmp]) } .height('100%') .width('100%') .backgroundColor('#F1F3F5') } } // PageOne.ets @Component export struct pageOneTmp { @Consume('pageInfos') pageInfos: NavPathStack; build() { NavDestination() { Column() { Text("NavDestinationContent1") }.width('100%').height('100%') }.title("NavDestinationTitle1") .onBackPressed(() => { const popDestinationInfo = this.pageInfos.pop() // 彈出路由棧棧頂元素 console.log('pop' + '返回值' + JSON.stringify(popDestinationInfo)) return true }) } } // PageTwo.ets @Component export struct pageTwoTmp { @Consume('pageInfos') pageInfos: NavPathStack; build() { NavDestination() { Column() { Text("NavDestinationContent2") }.width('100%').height('100%') }.title("NavDestinationTitle2") .onBackPressed(() => { const popDestinationInfo = this.pageInfos.pop() // 彈出路由棧棧頂元素 console.log('pop' + '返回值' + JSON.stringify(popDestinationInfo)) return true }) } } // PageThree.ets @Component export struct pageThreeTmp { @Consume('pageInfos') pageInfos: NavPathStack; build() { NavDestination() { Column() { Text("NavDestinationContent3") }.width('100%').height('100%') }.title("NavDestinationTitle3") .onBackPressed(() => { const popDestinationInfo = this.pageInfos.pop() // 彈出路由棧棧頂元素 console.log('pop' + '返回值' + JSON.stringify(popDestinationInfo)) return true }) } }
設定標題欄模式
標題欄在介面頂部,用於呈現介面名稱和操作入口,Navigation元件透過titleMode屬性設定標題欄模式。
-
Mini模式
普通型標題欄,用於一級頁面不需要突出標題的場景。
圖3 Mini模式標題欄
Navigation() { // ... } .titleMode(NavigationTitleMode.Mini)
-
Full模式
強調型標題欄,用於一級頁面需要突出標題的場景。
圖4 Full模式標題欄
Navigation() { // ... } .titleMode(NavigationTitleMode.Full)
設定選單欄
選單欄位於Navigation元件的右上角,開發者可以透過menus屬性進行設定。menus支援Array<NavigationMenuItem>和CustomBuilder兩種引數型別。使用Array<NavigationMenuItem>型別時,豎屏最多支援顯示3個圖示,橫屏最多支援顯示5個圖示,多餘的圖示會被放入自動生成的更多圖示。
圖5 設定了3個圖示的選單欄
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}} Navigation() { // ... } .menus([TooTmp, TooTmp, TooTmp])
圖片也可以引用resources中的資源。
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "resources/base/media/ic_public_highlights.svg", 'action': ()=> {}} Navigation() { // ... } .menus([TooTmp, TooTmp, TooTmp])
圖6 設定了4個圖示的選單欄
let TooTmp: NavigationMenuItem = {'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}} Navigation() { // ... } .menus([TooTmp, TooTmp, TooTmp, TooTmp])
設定工具欄
工具欄位於Navigation元件的底部,開發者可以透過toolbarConfiguration屬性進行設定。
圖7 工具欄
let TooTmp: ToolbarItem = {'value': "func", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {}} let TooBar: ToolbarItem[] = [TooTmp,TooTmp,TooTmp] Navigation() { // ... } .toolbarConfiguration(TooBar)
路由操作
Navigation路由相關的操作都是基於頁面棧NavPathStack提供的方法進行,每個Navigation都需要建立並傳入一個NavPathStack物件,用於管理頁面。主要涉及頁面跳轉、頁面返回、頁面替換、頁面刪除、引數獲取、路由攔截等功能。
從API version 12開始,頁面棧允許被繼承。開發者可以在派生類中自定義屬性和方法,也可以重寫父類的方法。派生類物件可以替代基類NavPathStack物件使用。具體示例程式碼參見:頁面棧繼承示例程式碼。
@Entry @Component struct Index { // 建立一個頁面棧物件並傳入Navigation pageStack: NavPathStack = new NavPathStack() build() { Navigation(this.pageStack) { } .title('Main') } }
頁面跳轉
NavPathStack透過Push相關的介面去實現頁面跳轉的功能,主要分為以下三類:
-
普通跳轉,透過頁面的name去跳轉,並可以攜帶param。
this.pageStack.pushPath({ name: "PageOne", param: "PageOne Param" }) this.pageStack.pushPathByName("PageOne", "PageOne Param")
-
帶返回回撥的跳轉,跳轉時新增onPop回撥,能在頁面出棧時獲取返回資訊,並進行處理。
this.pageStack.pushPathByName('PageOne', "PageOne Param", (popInfo) => { console.log('Pop page name is: ' + popInfo.info.name + ', result: ' + JSON.stringify(popInfo.result)) });
-
帶錯誤碼的跳轉,跳轉結束會觸發非同步回撥,返回錯誤碼資訊。
this.pageStack.pushDestinationByName('PageOne', "PageOne Param") .catch((error: BusinessError) => { console.error(`Push destination failed, error code = ${error.code}, error.message = ${error.message}.`); }).then(() => { console.error('Push destination succeed.'); });
頁面返回
NavPathStack透過Pop相關介面去實現頁面返回功能。
// 返回到上一頁 this.pageStack.pop() // 返回到上一個PageOne頁面 this.pageStack.popToName("PageOne") // 返回到索引為1的頁面 this.pageStack.popToIndex(1) // 返回到根首頁(清除棧中所有頁面) this.pageStack.clear()
頁面替換
NavPathStack透過Replace相關介面去實現頁面替換功能。
// 將棧頂頁面替換為PageOne this.pageStack.replacePath({ name: "PageOne", param: "PageOne Param" }) this.pageStack.replacePathByName("PageOne", "PageOne Param")
頁面刪除
NavPathStack透過Remove相關介面去實現刪除頁面棧中特定頁面的功能。
// 刪除棧中name為PageOne的所有頁面 this.pageStack.removeByName("PageOne") // 刪除指定索引的頁面 this.pageStack.removeByIndexes([1,3,5])
引數獲取
NavPathStack透過Get相關介面去獲取頁面的一些引數。
// 獲取棧中所有頁面name集合 this.pageStack.getAllPathName() // 獲取索引為1的頁面引數 this.pageStack.getParamByIndex(1) // 獲取PageOne頁面的引數 this.pageStack.getParamByName("PageOne") // 獲取PageOne頁面的索引集合 this.pageStack.getIndexByName("PageOne")
路由攔截
NavPathStack提供了setInterception方法,用於設定Navigation頁面跳轉攔截回撥。該方法需要傳入一個NavigationInterception物件,該物件包含三個回撥函式:
名稱 |
描述 |
---|---|
willShow |
頁面跳轉前回撥,允許操作棧,在當前跳轉生效。 |
didShow |
頁面跳轉後回撥,在該回撥中操作棧會在下一次跳轉生效。 |
modeChange |
Navigation單雙欄顯示狀態發生變更時觸發該回撥。 |
無論是哪個回撥,在進入回撥時頁面棧都已經發生了變化。
開發者可以在willShow回撥中透過修改路由棧來實現路由攔截重定向的能力。
this.pageStack.setInterception({ willShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar", operation: NavigationOperation, animated: boolean) => { if (typeof to === "string") { console.log("target page is navigation home page."); return; } // 將跳轉到PageTwo的路由重定向到PageOne let target: NavDestinationContext = to as NavDestinationContext; if (target.pathInfo.name === 'PageTwo') { target.pathStack.pop(); target.pathStack.pushPathByName('PageOne', null); } } })
子頁面
NavDestination是Navigation子頁面的根容器,用於承載子頁面的一些特殊屬性以及生命週期等。NavDestination可以設定獨立的標題欄和選單欄等屬性,使用方法與Navigation相同。NavDestination也可以透過mode屬性設定不同的顯示型別,用於滿足不同頁面的訴求。
頁面顯示型別
-
標準型別
NavDestination元件預設為標準型別,此時mode屬性為NavDestinationMode.STANDARD。標準型別的NavDestination的生命週期跟隨其在NavPathStack頁面棧中的位置變化而改變。
-
彈窗型別
NavDestination設定mode為NavDestinationMode.DIALOG彈窗型別,此時整個NavDestination預設透明顯示。彈窗型別的NavDestination顯示和消失時不會影響下層標準型別的NavDestination的顯示和生命週期,兩者可以同時顯示。
// Dialog NavDestination @Entry @Component struct Index { @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack() @Builder PagesMap(name: string) { if (name == 'DialogPage') { DialogPage() } } build() { Navigation(this.pageStack) { Button('Push DialogPage') .margin(20) .width('80%') .onClick(() => { this.pageStack.pushPathByName('DialogPage', ''); }) } .mode(NavigationMode.Stack) .title('Main') .navDestination(this.PagesMap) } } @Component export struct DialogPage { @Consume('NavPathStack') pageStack: NavPathStack; build() { NavDestination() { Stack({ alignContent: Alignment.Center }) { Column() { Text("Dialog NavDestination") .fontSize(20) .margin({ bottom: 100 }) Button("Close").onClick(() => { this.pageStack.pop() }).width('30%') } .justifyContent(FlexAlign.Center) .backgroundColor(Color.White) .borderRadius(10) .height('30%') .width('80%') }.height("100%").width('100%') } .backgroundColor('rgba(0,0,0,0.5)') .hideTitleBar(true) .mode(NavDestinationMode.DIALOG) } }
頁面生命週期
Navigation作為路由容器,其生命週期承載在NavDestination元件上,以元件事件的形式開放。
其生命週期大致可分為三類,自定義元件生命週期、通用元件生命週期和自有生命週期。其中,aboutToAppear和aboutToDisappear是自定義元件的生命週期(NavDestination外層包含的自定義元件),OnAppear和OnDisappear是元件的通用生命週期。剩下的六個生命週期為NavDestination獨有。
生命週期時序如下圖所示:
- aboutToAppear:在建立自定義元件後,執行其build()函式之前執行(NavDestination建立之前),允許在該方法中改變狀態變數,更改將在後續執行build()函式中生效。
- onWillAppear:NavDestination建立後,掛載到元件樹之前執行,在該方法中更改狀態變數會在當前幀顯示生效。
- onAppear:通用生命週期事件,NavDestination元件掛載到元件樹時執行。
- onWillShow:NavDestination元件佈局顯示之前執行,此時頁面不可見(應用切換到前臺不會觸發)。
- onShown:NavDestination元件佈局顯示之後執行,此時頁面已完成佈局。
- onWillHide:NavDestination元件觸發隱藏之前執行(應用切換到後臺不會觸發)。
- onHidden:NavDestination元件觸發隱藏後執行(非棧頂頁面push進棧,棧頂頁面pop出棧或應用切換到後臺)。
- onWillDisappear:NavDestination元件即將銷燬之前執行,如果有轉場動畫,會在動畫前觸發(棧頂頁面pop出棧)。
- onDisappear:通用生命週期事件,NavDestination元件從元件樹上解除安裝銷燬時執行。
- aboutToDisappear:自定義元件析構銷燬之前執行,不允許在該方法中改變狀態變數。
頁面監聽和查詢
為了方便元件跟頁面解耦,在NavDestination子頁面內部的自定義元件可以透過全域性方法監聽或查詢到頁面的一些狀態資訊。
-
頁面資訊查詢
自定義元件提供queryNavDestinationInfo方法,可以在NavDestination內部查詢到當前所屬頁面的資訊,返回值為NavDestinationInfo,若查詢不到則返回undefined。
import { uiObserver } from '@kit.ArkUI'; // NavDestination內的自定義元件 @Component struct MyComponent { navDesInfo: uiObserver.NavDestinationInfo | undefined aboutToAppear(): void { this.navDesInfo = this.queryNavDestinationInfo(); } build() { Column() { Text("所屬頁面Name: " + this.navDesInfo?.name) }.width('100%').height('100%') } }
-
頁面狀態監聽
透過observer.on('navDestinationUpdate')提供的註冊介面可以註冊NavDestination生命週期變化的監聽,使用方式如下:
uiObserver.on('navDestinationUpdate', (info) => { console.info('NavDestination state update', JSON.stringify(info)); });
也可以註冊頁面切換的狀態回撥,能在頁面發生路由切換的時候拿到對應的頁面資訊NavDestinationSwitchInfo,並且提供了UIAbilityContext和UIContext不同範圍的監聽:
// 在UIAbility中使用 import { UIContext, uiObserver } from '@kit.ArkUI'; // callBackFunc 是開發者定義的監聽回撥函式 function callBackFunc(info: uiObserver.NavDestinationSwitchInfo) {} uiObserver.on('navDestinationSwitch', this.context, callBackFunc); // 可以透過視窗的getUIContext()方法獲取對應的UIContent uiContext: UIContext | null = null; uiObserver.on('navDestinationSwitch', this.uiContext, callBackFunc);
頁面轉場
Navigation預設提供了頁面切換的轉場動畫,透過頁面棧操作時,會觸發不同的轉場效果(Dialog型別的頁面預設無轉場動畫),Navigation也提供了關閉系統轉場、自定義轉場以及共享元素轉場的能力。
關閉轉場
-
全域性關閉
Navigation透過NavPathStack中提供的disableAnimation方法可以在當前Navigation中關閉或開啟所有轉場動畫。
pageStack: NavPathStack = new NavPathStack() aboutToAppear(): void { this.pageStack.disableAnimation(true) }
-
單次關閉
NavPathStack中提供的Push、Pop、Replace等介面中可以設定animated引數,預設為true表示有轉場動畫,需要單次關閉轉場動畫可以置為false,不影響下次轉場動畫。
pageStack: NavPathStack = new NavPathStack() this.pageStack.pushPath({ name: "PageOne" }, false) this.pageStack.pop(false)
自定義轉場
Navigation透過customNavContentTransition事件提供自定義轉場動畫的能力,透過如下三步可以定義一個自定義的轉場動畫。
- 構建一個自定義轉場動畫工具類CustomNavigationUtils,透過一個Map管理各個頁面自定義動畫物件CustomTransition,頁面在建立的時候將自己的自定義轉場動畫物件註冊進去,銷燬的時候解註冊;
- 實現一個轉場協議物件NavigationAnimatedTransition,其中timeout屬性表示轉場結束的超時時間,預設為1000ms,transition屬性為自定義的轉場動畫方法,開發者要在這裡實現自己的轉場動畫邏輯,系統會在轉場開始時呼叫該方法,onTransitionEnd為轉場結束時的回撥。
- 呼叫customNavContentTransition方法,返回實現的轉場協議物件,如果返回undefined,則使用系統預設轉場。
具體示例程式碼可以參考Navigation自定義轉場示例。
共享元素轉場
NavDestination之間切換時可以透過geometryTransition實現共享元素轉場。配置了共享元素轉場的頁面同時需要關閉系統預設的轉場動畫。
-
為需要實現共享元素轉場的元件新增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')
跨包動態路由
透過靜態import頁面再進行路由跳轉的方式會造成不同模組之間的依賴耦合,以及首頁載入時間長等問題。
動態路由設計的目的就是為了解決多個模組(HAR/HSP)之間可以複用相同的業務,各個業務模組之間解耦和路由功能擴充套件整合。
動態路由的優勢:
- 路由定義除了跳轉的URL以外,可以豐富的配置擴充套件資訊,如橫豎屏預設模式,是否需要鑑權等等,做路由跳轉時統一處理。
- 給每個路由頁面設定一個名字,按照名稱進行跳轉而不是檔案路徑。
- 頁面的載入可以使用動態Import(按需載入),防止首個頁面載入大量程式碼導致卡頓。
動態路由提供系統路由表和自定義路由表兩種方式。
-
系統路由表相對自定義路由表,使用更簡單,只需要新增對應頁面跳轉配置項,即可實現頁面跳轉。
-
自定義路由表使用起來更復雜,但是可以根據應用業務進行定製處理。
支援自定義路由表和系統路由表混用。
系統路由表
從API version 12開始,Navigation支援使用系統路由表的方式進行動態路由。各業務模組(HSP/HAR)中需要獨立配置route_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獲取。
-
在跳轉目標頁面中,需要配置入口Builder函式,函式名稱需要和route_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 }) } }
-
透過pushPathByName等路由介面進行頁面跳轉。(注意:此時Navigation中可以不用配置navDestination屬性)。
@Entry @Component struct Index { pageStack : NavPathStack = new NavPathStack(); build() { Navigation(this.pageStack){ }.onAppear(() => { this.pageStack.pushPathByName("PageOne", null, false); }) .hideNavBar(true) } }
自定義路由表
開發者可以透過自定義路由表的方式來實現跨包動態路由,具體實現方法請參考Navigation自定義動態路由 示例。
實現方案:
- 定義頁面跳轉配置項。
- 使用資原始檔進行定義,透過資源管理@ohos.resourceManager在執行時對資原始檔解析。
- 在ets檔案中配置路由載入配置項,一般包括路由頁面名稱(即pushPath等介面中頁面的別名),檔案所在模組名稱(hsp/har的模組名),載入頁面在模組內的路徑(相對src目錄的路徑)。
- 載入目標跳轉頁面,透過動態import將跳轉目標頁面所在的模組在執行時載入, 在模組載入完成後,呼叫模組中的方法,透過import在模組的方法中載入模組中顯示的目標頁面,並返回頁面載入完成後定義的Builder函式。
- 觸發頁面跳轉,在Navigation的navDestination屬性執行步驟2中載入的Builder函式,即可跳轉到目標頁面。
示例程式碼
- Navigation系統路由