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)