鸿蒙掌上驾考宝典应用开发34:折叠屏适配——多设备形态响应式设计
第34篇:折叠屏适配——多设备形态响应式设计

一、引言
鸿蒙系统的一大特色就是支持多种设备形态:手机、折叠屏(包括阔折叠和双折叠)、平板等。DriverLicenseExam 项目在设计之初就考虑到了多设备适配,通过安全区域处理、断点模型、响应式布局等技术手段,实现了在不同屏幕尺寸上的一致体验。本文将深入解析多设备适配的技术实现。
二、设备形态与适配难点
2.1 需要适配的设备类型
项目 README 中明确列出了支持的设备类型:
- 华为手机:常规直板手机
- 双折叠:折叠屏展开状态
- 阔折叠:横向展开的大屏状态
- 平板(tablet):更大屏幕尺寸
2.2 适配的核心挑战
手机(竖屏) 折叠屏(展开) 平板
┌──────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ │ │ │ │ │
│ 窄屏 │ │ 宽屏 │ │ 宽屏 │
│ │ │ │ │ │
│ │ │ │ │ │
└──────┘ └──────────────┘ └──────────────┘
360px 800px+ 1200px+
适配的核心挑战是:同一个页面布局在不同的屏幕宽度下,如何自动调整以提供最佳用户体验。
三、安全区域处理
3.1 为什么需要安全区域
鸿蒙设备有不同的导航方式:手势导航(底部横条)和三键导航。不同导航方式下,系统 UI 占用的区域不同。此外,折叠屏设备的铰链区域、打孔屏的前置摄像头区域都需要避让。
3.2 获取安全区域
在 EntryAbility 中,通过 window.getWindowAvoidArea 获取安全区域数据:
// EntryAbility.ets - 获取安全区域
public onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/MainEntry', (err) => {
if (err.code) {
Logger.error(TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
try {
AppStorage.setOrCreate('windowStage', windowStage);
let windowClass: window.Window = windowStage.getMainWindowSync();
// 设置窗口全屏
let isLayoutFullScreen = true;
windowClass.setWindowLayoutFullScreen(isLayoutFullScreen);
// 获取底部导航指示器避让区域
let type = window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR;
let avoidArea = windowClass.getWindowAvoidArea(type);
let bottomRectHeight = avoidArea.bottomRect.height;
AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight);
// 获取顶部状态栏避让区域
type = window.AvoidAreaType.TYPE_SYSTEM;
avoidArea = windowClass.getWindowAvoidArea(type);
let topRectHeight = avoidArea.topRect.height;
AppStorage.setOrCreate('topRectHeight', topRectHeight);
} catch (e) {
Logger.error(TAG, 'loadContent post-success error: %{public}s', JSON.stringify(e) ?? '');
}
});
}
3.3 动态监听避让区域变化
当用户在"手势导航"和"三键导航"之间切换时,避让区域会发生变化。项目通过 avoidAreaChange 事件实时监听:
// 注册监听函数,动态获取避让区数据
windowClass.on('avoidAreaChange', (data) => {
try {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
// 顶部状态栏高度变化
let topRectHeight = data.area.topRect.height;
AppStorage.setOrCreate('topRectHeight', topRectHeight);
} else if (data.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
// 底部导航指示器高度变化
let bottomRectHeight = data.area.bottomRect.height;
AppStorage.setOrCreate('bottomRectHeight', bottomRectHeight);
}
} catch (e) {
Logger.error(TAG, 'avoidAreaChange handler error: %{public}s', JSON.stringify(e) ?? '');
}
});
3.4 在页面中应用安全区域
获取到安全区域数据后,通过 AppStorage 共享给所有页面:
// 页面中使用避让区域
build() {
Stack() {
// 页面内容
Scroll() {
Column() {
// ...
}
.padding({
top: this.getUIContext().px2vp(AppStorage.get('topRectHeight') || 0) + 10,
left: 16,
right: 16,
});
}
}
.width('100%')
.height('100%');
}
// 底部导航栏应用避让
.padding({
bottom: this.getUIContext().px2vp(this.bottomRectHeight)
})
这里的关键点是 px2vp 转换:getWindowAvoidArea 返回的是 px 单位,而 ArkUI 的布局使用 vp 单位,需要通过 px2vp 进行转换。
四、折叠屏的特殊处理
4.1 折叠状态变化
折叠屏设备在展开和折叠时,屏幕宽度会发生变化。项目通过监听窗口尺寸变化来适配:
// 在页面中获取当前窗口宽度
aboutToAppear() {
this.bottomRectHeight = AppStorage.get('bottomRectHeight') || 0;
this.topRectHeight = AppStorage.get('topRectHeight') || 0;
}
4.2 布局自适应
项目中多处使用 layoutWeight 实现自适应布局:
// 搜索框使用 layoutWeight 自适应宽度
Row() {
// 城市选择区域
Row() {
Text(this.guideService.getGuideData().city).fontSize(14);
Image($r('app.media.city_triangle')).width(12).height(12);
}
.margin({ left: 12, right: 8 });
// 搜索框占据剩余空间
Row({ space: 8 }) {
Image($r('app.media.ic_glass')).width(16);
Text('请输入关键字搜索').fontSize(14);
}
.layoutWeight(1) // 自动占据剩余空间
.margin({ right: 12 });
}
在手机模式下,城市选择 + 搜索框正好填满顶部栏。在折叠屏展开模式下,搜索框会自动拉伸填充多出的空间。
五、响应式布局
5.1 断点模型
app_setting 组件中的 BreakpointModel 实现了断点判断:
// BreakpointModel.ets
export class BreakpointModel {
static getBreakpoint(width: number): string {
if (width < 520) return 'sm'; // 手机
if (width < 840) return 'md'; // 折叠屏展开
return 'lg'; // 平板
}
// 根据断点返回列数
static getColumns(breakpoint: string): number {
switch (breakpoint) {
case 'sm': return 1; // 手机:单列
case 'md': return 2; // 折叠屏:双列
case 'lg': return 3; // 平板:三列
default: return 1;
}
}
}
5.2 GridRow/GridCol 响应式网格
项目使用 GridRow 和 GridCol 实现响应式网格布局:
// 设置页面的响应式布局
GridRow() {
GridCol({ span: { sm: 12, md: 8, lg: 6 } }) {
Column() {
// 设置项内容
this.settingItem('隐私协议', () => {});
this.settingItem('保密设置', () => {});
this.settingItem('退出登录', () => {});
}
.width('100%');
}
}
span 属性使用对象语法,为不同断点指定不同的列数:
| 断点 | 屏幕宽度 | span | 说明 |
|---|---|---|---|
| sm | < 520px | 12 | 手机全宽 |
| md | 520~840px | 8 | 折叠屏 2/3 宽 |
| lg | > 840px | 6 | 平板 1/2 宽 |
5.3 组件级自适应
除了全局布局,单个组件也实现了自适应:
// 设置卡片组件
@ComponentV2
export struct SettingCard {
build() {
Column() {
// 内容
}
.width('100%')
.borderRadius(16)
.backgroundColor($r('sys.color.comp_background_primary'));
}
}
使用 width('100%') 结合父容器的宽度控制,确保组件自动填充可用空间。
六、代码中的适配实践汇总
6.1 px2vp 转换
项目中多次使用 px2vp 将像素值转换为视图点:
// px → vp 转换
this.getUIContext().px2vp(AppStorage.get('topRectHeight') || 0)
this.getUIContext().px2vp(AppStorage.get('bottomRectHeight') || 0)
this.getUIContext().px2fp(64) // 字体大小转换
6.2 百分比宽度
// 使用百分比宽度,自动适配屏幕
.width('100%')
.height('100%')
.width(CommonConstants.FULL_PERCENT)
6.3 弹性空间分配
// Blank 填充剩余空间
Row() {
Text('隐私协议').fontSize(16);
Blank(); // 将箭头推到右侧
Image($r('app.media.ic_right_arrow_lined')).width(7).height(14);
}
.width('100%');
// layoutWeight 比例分配
Row() {
Image($r('app.media.ic_glass')).width(16);
Text('请输入关键字搜索').fontSize(14);
}
.layoutWeight(1); // 占据剩余空间
七、适配检测清单
在开发多设备适配时,可以参照以下清单进行检测:
- 安全区域:顶部状态栏、底部导航指示器是否避让
- 安全区域变化:手势导航/三键导航切换时布局是否正常
- 折叠屏:展开/折叠切换时布局是否自动调整
- 字体缩放:大字体模式下布局是否溢出
- 图片适配:不同屏幕密度下图片是否清晰
- 触摸区域:按钮点击区域是否适配不同手指尺寸
- 横竖屏:横屏模式下布局是否合理
八、总结
折叠屏适配是鸿蒙应用开发的重要课题。DriverLicenseExam 项目通过以下技术实现了多设备适配:
- 安全区域获取:通过
getWindowAvoidArea获取系统 UI 避让区域 - 动态监听:通过
avoidAreaChange事件实时响应导航方式变化 - 断点模型:根据屏幕宽度返回不同断点(sm/md/lg)
- 响应式网格:GridRow/GridCol 实现不同断点下的布局变化
- 弹性布局:layoutWeight、Blank、百分比宽度等实现自适应
这些技术不仅适用于折叠屏适配,也是构建高质量鸿蒙应用的通用技能。
关键源码文件:
products/entry/src/main/ets/entryability/EntryAbility.ets— 安全区域获取components/app_setting/src/main/ets/models/BreakpointModel.ets— 断点模型components/app_setting/src/main/ets/common/GridRowColSetting.ets— 响应式网格products/entry/src/main/ets/pages/home/HomeView.ets— 自适应布局示例products/entry/src/main/ets/pages/MainEntry.ets— 底部安全区域应用
更多推荐





所有评论(0)