• Home
Name Date Size #Lines LOC

..--

AppScope/06-May-2025-3432

entry/06-May-2025-1,084986

hvigor/06-May-2025-2322

screenshots/06-May-2025-

.gitignoreD06-May-2025133 1212

README_zh.mdD06-May-202513 KiB315279

build-profile.json5D06-May-20251.3 KiB5756

code-linter.json5D06-May-20251.4 KiB4746

hvigorfile.tsD06-May-2025843 225

oh-package.json5D06-May-2025809 2624

ohosTest.mdD06-May-20251.4 KiB1310

README_zh.md

1# Navigation实现折叠屏适配案例
2
3### 介绍
4
5本示例展示了如何利用`Navigation`组件的`mode`属性来适配折叠屏设备,主要实现步骤是通过监听主窗口的尺寸变化和路由栈的动态变化来动态改变`Navigation`组件的`mode`属性,为用户带来更加优化和个性化的交互体验。
6
7### 效果图预览
8
9| 首页-折叠                                                          | 首页-展开                                                      |
10|----------------------------------------------------------------|------------------------------------------------------------|
11| <img src="screenshots/home_fold.png" width="300" />            | <img src="screenshots/home_expand.png" width="300" />      |
12| 详情页-折叠-普通                                                      | 详情页-折叠-全屏                                                  |
13| <img src="screenshots/detail_fold_normal.png" width="300" />   | <img src="screenshots/detail_fold_full.png" width="300" /> |
14| 详情页-展开-普通                                                      | 详情页-展开-全屏                                                  |
15| <img src="screenshots/detail_expand_normal.png" width="300" /> | <img src="screenshots/detail_expand_full.png" width="300" />        |
16
17### 使用说明
18
191. 在折叠屏上安装应用
202. 查看折叠/展开状态下的首页
213. 查看折叠/展开状态下的详情页
22
23### 工程目录
24
25```
26|entry/src/main/ets
27|   |---entryablity
28|   |   |---EntryAbility.ts                         // 程序入口类
29|   |---constants                                   // 常量
30|   |---utils                                       // 工具类
31|   |---pages                                       // 示例使用
32|   |   |---HomePage.ets                            // 首页
33|   |   |---NormalPage.ets                          // 普通页面
34|   |   |---FullScreenPage.ets                      // 全屏页面
35```
36
37### 实现思路
38
391. 在`EntryAbility`中设置窗口尺寸变化监听并使用`AppStorage`存储。源码参考:[EntryAbility.ets](./entry/src/main/ets/entryability/EntryAbility.ets)
40   ```ts
41   onWindowStageCreate(windowStage: window.WindowStage): void {
42     // 获取默认窗口
43     this.mWindow = windowStage.getMainWindowSync();
44     this.updateBreakpoint(this.mWindow.getWindowProperties().windowRect.width);
45     // 订阅窗口尺寸变化
46     this.mWindow.on('windowSizeChange', (size: window.Size) => {
47       this.updateBreakpoint(size.width);
48       AppStorage.setOrCreate('windowSize', size);
49     });
50     ...
51   }
52   private updateBreakpoint(windowWidth: number): void {
53     let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels;
54     let curBp: string = '';
55     if (windowWidthVp < BreakpointConstants.BREAKPOINT_RANGES[1]) {
56       curBp = BreakpointConstants.BREAKPOINT_SM;
57     } else if (windowWidthVp < BreakpointConstants.BREAKPOINT_RANGES[2]) {
58       curBp = BreakpointConstants.BREAKPOINT_MD;
59     } else {
60       curBp = BreakpointConstants.BREAKPOINT_LG;
61     }
62     AppStorage.setOrCreate('currentBreakpoint', curBp);
63   }
64   ```
65
662. 自定义应用状态会发生改变的三种模式:设备折叠态发生改变或屏幕旋转、应用从首页进入子路由以及应用从子路由返回首页,并对模式改变进行监听便于进一步处理。源码参考:[HomePage.ets](./entry/src/main/ets/pages/HomePage.ets)和[RouterUtils.ets](./entry/src/main/ets/utils/RouterUtils.ets)
67    ```ts
68   // 设置导航栏显示改变模式枚举值
69   export enum NavMode {
70     DefaultMode, // 默认模式
71     FoldMode, // 折叠模式
72     ChildPageMode, // 进入子页面模式
73     HomePageMode // 返回首页模式
74   }
75
76   // 定义初始模式为NavMode.DefaultMode,并设置监听函数onModeChange
77   @State @Watch('onModeChange') navMode: NavMode = NavMode.DefaultMode;
78
79   // 监听模式改变
80   onModeChange() {
81     let lastRouteName :string= this.pageStack.getAllPathName()[this.pageStack.getAllPathName().length-1];
82     switch (this.navMode) {
83       // 当设备折叠态发生改变或屏幕旋转时响应以下逻辑
84       case NavMode.FoldMode:
85         // 全屏案例在折叠态变化时不需要切换NavigationMode
86         if (FULL_SCREEN_ROUTE.includes(lastRouteName)) {
87           this.navigationMode = NavigationMode.Stack;
88           break;
89         }
90         if (this.currentBreakpoint !== BreakpointConstants.BREAKPOINT_SM) {
91           if (this.pageStack.size() > 0) {
92             // 宽屏条件下且展示了子路由,NavigationMode为Split
93             this.navigationMode = NavigationMode.Split;
94             this.swiperDisplayCount = 1;
95           } else {
96             // 宽屏条件下且未展示子路由,NavigationMode为Stack
97             this.navigationMode = NavigationMode.Stack;
98             this.swiperDisplayCount = 2;
99           }
100         } else {
101           this.navigationMode = NavigationMode.Stack;
102           this.swiperDisplayCount = 1;
103         }
104         break;
105       // 当应用进入子路由时响应以下逻辑
106       case NavMode.ChildPageMode:
107         // 进入全屏案例需切换为Stack
108         if (FULL_SCREEN_ROUTE.includes(this.enterRouteName)) {
109           this.navigationMode = NavigationMode.Stack;
110           break;
111         }
112         // 根据屏幕宽度决定NavigationMode
113         if (this.currentBreakpoint !== BreakpointConstants.BREAKPOINT_SM) {
114           this.navigationMode = NavigationMode.Split;
115         } else {
116           this.navigationMode = NavigationMode.Stack;
117         }
118         this.swiperDisplayCount = 1;
119         break;
120       // 当应用返回首页时响应以下逻辑
121       case NavMode.HomePageMode:
122         if (this.currentBreakpoint !== BreakpointConstants.BREAKPOINT_SM) {
123           this.navigationMode = NavigationMode.Stack;
124           this.swiperDisplayCount = 2;
125         } else {
126           this.navigationMode = NavigationMode.Stack;
127           this.swiperDisplayCount = 1;
128         }
129         this.pageStack.disableAnimation(false);
130         break;
131       default:
132         break;
133     }
134     // 重置NavMode
135     if (this.navMode !== NavMode.DefaultMode) {
136        this.navMode = NavMode.DefaultMode;
137     }
138   }
139   ```
140
1413. 定义兼容折叠屏的路由跳转和退出逻辑,根据不同的屏幕宽度进行合适的路由操作。源码参考:[RouterUtils.ets](./entry/src/main/ets/utils/RouterUtils.ets)
142    ```ts
143   export class RouterUtils {
144     public static pageStack: NavPathStack = new NavPathStack();
145     // 全屏子路由
146     private static fullScreenRouter: string[] = [];
147     private static timer: number = 0;
148
149     public static setFullScreenRouter(fullScreenRouter: string[]) {
150       RouterUtils.fullScreenRouter = fullScreenRouter;
151     }
152
153     // 通过获取页面栈并pop
154     public static popAppRouter(): void {
155       if (RouterUtils.pageStack.getAllPathName().length > 1) {
156         RouterUtils.pageStack.pop();
157       } else {
158         logger.info('RouterStack is only Home.');
159       }
160       // 定义emitter事件
161       let innerEvent: emitter.InnerEvent = {
162         eventId: 3
163       };
164       let eventData: emitter.EventData = {
165         data: {
166           navMode: NavMode.HomePageMode
167         }
168       };
169       let allPathName: string[] = RouterUtils.pageStack.getAllPathName();
170       // 查找到对应的路由栈进行pop
171       if (!RouterUtils.fullScreenRouter.includes(allPathName[allPathName.length-1]) &&
172         RouterUtils.pageStack.size() === 1) {
173         // 非全屏子路由宽屏条件下回到首页,Navigation的mode属性修改默认动画会与过场动画冲突,需关闭过场动画
174         if (display.getDefaultDisplaySync().width > DEFAULT_WINDOW_SIZE.width) {
175           RouterUtils.pageStack.disableAnimation(true);
176         }
177         RouterUtils.timer = setTimeout(() => {
178           // 触发EntryView下navMode改变
179           emitter.emit(innerEvent, eventData);
180         }, DELAY_TIME);
181         RouterUtils.pageStack.pop();
182       } else if (RouterUtils.fullScreenRouter.includes(allPathName[allPathName.length-1])) {
183         // 全屏子路由返回逻辑
184         RouterUtils.pageStack.pop();
185         // 触发EntryView下navMode改变
186         emitter.emit(innerEvent, eventData);
187       } else {
188         RouterUtils.pageStack.pop();
189       }
190     }
191
192     /**
193      * 兼容折叠屏下的路由跳转
194      * @param uri 路由名称
195      * @param param 路由参数
196      */
197     public static pushUri(uri: string, param?: ESObject) {
198       // 记录当前进入路由名称
199       AppStorage.setOrCreate('enterRouteName', uri);
200       // 定义emitter事件
201       let innerEvent: emitter.InnerEvent = {
202         eventId: 3
203       };
204       let eventData: emitter.EventData = {
205         data: {
206           navMode: NavMode.ChildPageMode
207         }
208       };
209       // 触发EntryView下navMode改变
210       emitter.emit(innerEvent, eventData);
211       // 获取当前窗口宽度
212       let displayInfo: display.Display = display.getDefaultDisplaySync();
213       let windowSize: window.Size | undefined =
214         AppStorage.get<window.Size>('windowSize') !== undefined ? AppStorage.get<window.Size>('windowSize') : {
215           width: displayInfo.width,
216           height: displayInfo.height
217         } as window.Size;
218       // 宽屏条件下跳转
219       if (windowSize!.width > DEFAULT_WINDOW_SIZE.width) {
220         RouterUtils.pageStack.clear();
221         if (RouterUtils.timer) {
222           clearTimeout(RouterUtils.timer);
223         }
224         // Navigation的mode属性修改会有一段响应时间,需延时跳转
225         RouterUtils.timer = setTimeout(() => {
226           RouterUtils.pageStack.pushPathByName(uri, param);
227         }, DELAY_TIME);
228       } else {
229         RouterUtils.pageStack.pushPathByName(uri, param);
230       }
231     }
232   }
233   ```
234
235### 相关权限
236
237不涉及
238
239### 依赖
240
241不涉及
242
243### FAQ
244
245#### 1.  首页swiper的宽度为什么要根据显示的item数量动态计算而不是直接使用100%?
246如果直接使用100%首页Navigation切换mode属性时swiper会有缩放闪烁问题,根据显示的item数量动态计算swiper的宽度swiper不会闪烁且item宽度合理。源码参考:[HomePage.ets](./entry/src/main/ets/pages/HomePage.ets)
247   ```ts
248   Swiper() {
249     ForEach(this.swiperData, (dataItem: ResourceStr) => {
250       Image(dataItem)
251         .width(this.swiperDisplayCount === 2 ? px2vp(this.windowSize.width / 2) : 353)
252         ...
253     })
254   }
255   .id("MainSwiper")
256   .autoPlay(true)
257   .displayCount(this.swiperDisplayCount)
258   .margin({ top: 8, bottom: 8 })
259   .width(this.swiperDisplayCount === 2 ? px2vp(this.windowSize.width) : 353)
260   ```
261
262#### 2.  折叠屏展开条件下从首页进行路由跳转为什么要使用延时?
263这是因为Navigation修改完mode属性后有一段反应时长,不能够马上路由跳转,否则子路由页面会从屏幕左侧滑动进入,理想效果是Navigation先分栏子路由页面再从屏幕中间滑动进入。源码参考:[RouterUtils.ets](./entry/src/main/ets/utils/RouterUtils.ets)
264   ```ts
265   // 宽屏条件下跳转
266   if (windowSize!.width > DEFAULT_WINDOW_SIZE.width) {
267     RouterUtils.pageStack.clear();
268     if (RouterUtils.timer) {
269       clearTimeout(RouterUtils.timer);
270     }
271     // Navigation的mode属性修改会有一段响应时间,需延时跳转
272     RouterUtils.timer = setTimeout(() => {
273       RouterUtils.pageStack.pushPathByName(uri, param);
274     }, DELAY_TIME);
275   } else {
276     RouterUtils.pageStack.pushPathByName(uri, param);
277   }
278   ```
279
280#### 3. windowSize的初始值为什么要使用`display.getDefaultDisplaySync()`获取的值而不是从AppStorage中取值?
281AppStorage中的windowSize要屏幕发生改变才会赋值,所以初始值需要使用display接口获取。源码参考:[HomePage.ets](./entry/src/main/ets/pages/HomePage.ets)
282   ```ts
283   @StorageProp('windowSize') windowSize: window.Size = {
284     width: display.getDefaultDisplaySync().width,
285     height: display.getDefaultDisplaySync().height
286   };
287   ```
288
289### 约束与限制
290
2911. 本示例仅支持标准系统折叠屏设备上运行。
292
2932. 本示例为Stage模型,从API version 12开始支持。SDK版本号:5.0.0.71 Release,镜像版本号:OpenHarmony 5.0.1.107。
294
2953. 本示例需要使用DevEco Studio 5.0.2 Release (Build Version: 5.0.7.200, built on January 23, 2025)编译运行。
296
297### 下载
298
299如需单独下载本工程,执行如下命令:
300
301```shell
302git init
303git config core.sparsecheckout true
304echo code/UI/FoldableAdaptation/ > .git/info/sparse-checkout
305git remote add origin https://gitee.com/openharmony/applications_app_samples.git
306git pull origin master
307```
308
309### 参考资料
310
311[Navigation](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-navigation.md)
312
313[windowClass.on('onWindowSizeChange')](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/js-apis-window.md#onwindowsizechange7)
314
315[display.getDefaultDisplaySync](https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-arkui/js-apis-display.md#displaygetdefaultdisplaysync9)