第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 项目通过以下技术实现了多设备适配:

  1. 安全区域获取:通过 getWindowAvoidArea 获取系统 UI 避让区域
  2. 动态监听:通过 avoidAreaChange 事件实时响应导航方式变化
  3. 断点模型:根据屏幕宽度返回不同断点(sm/md/lg)
  4. 响应式网格:GridRow/GridCol 实现不同断点下的布局变化
  5. 弹性布局: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 — 底部安全区域应用
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐