一、悬停态是啥玩意儿

折叠屏有个独特的手持操作体验叫"悬停态"。啥意思?用户可以把设备半折后立在桌面上,实现免手持体验。

悬停态适用于不需要频繁交互的任务:

  • 视频通话
  • 视频播放
  • 拍照
  • 听歌

进入悬停态时,有个问题:中间弯折区域难以操作,且显示内容会变形。所以页面内容要进行折痕区避让适配。

上图展示了折叠屏悬停态的使用场景,设备半折立在桌面上,上半屏显示视频,下半屏显示控制按钮。


二、三种实现方式对比

实现悬停态有三种方式:

对比项 FolderStack FoldSplitContainer 自定义实现
展开态/折叠态是否支持自定义布局 支持 不支持,固定二分栏/三分栏 支持
是否支持由其他页面进入悬停态页面 支持 支持 支持
是否支持自定义设备状态进入悬停态页面 不支持 不支持 支持
是否支持自定义悬停态窗口旋转策略 不支持 不支持 支持
开发难度 简单 简单 困难

三种方式各有特点:

  • FolderStack:使用简单,无需关注设备状态,支持自定义页面布局
  • FoldSplitContainer:使用简单,但固定的二分栏和三分栏布局限制了使用场景
  • 自定义实现:需要自行监听设备状态并调整组件布局,支持自定义布局,且由于自实现悬停态监听,可以限制设备进入悬停态的场景(例如仅允许在横屏下半折叠时进入悬停态)以及自定义窗口旋转策略,使用更加灵活


三、FolderStack 实现:最简单的方式

实现原理

FolderStack 是系统提供的 ArkTS 组件,继承自层叠布局 Stack。在 Stack 组件的基础上,FolderStack 提供监控设备是否进入悬停态并进行重新布局的能力。

FolderStack 通过 upperItems 字段来实现悬停态布局:

  • 被 upperItems 字段修饰的组件会堆叠在上半屏
  • 其他未被修饰的组件会堆叠在下半屏,并且自动避让折叠屏折痕区

注意:FolderStack 需要撑满页面全屏,如果不撑满页面全屏,则只作为普通 Stack 使用。

适用场景

适用于视频全屏播放等交互少的场景。

开发步骤

以视频播放类应用的全屏播放页面为例,将页面的父容器设置为 FolderStack,并将视频播放组件的 ID 注册到 upperItems 数组中。

悬停态时:

  • 视频播放组件会自动调整到上半屏显示
  • 视频控制组件和顶部返回组件则显示在下半屏

代码实现:

FolderStack({ upperItems: ['upper'] }) {
  // 视频播放组件,显示在上半屏
  VideoPlayView({ avPlayerUtil: this.avPlayerUtil })
    .id('upper')

  // 视频控制组件,显示在下半屏
  VideoControlView({ avPlayerUtil: this.avPlayerUtil })

  // 顶部返回组件,显示在下半屏
  BackTitleView({
    title: Const.PAGE_TITLES[0]
  })
}

关键点:

  • upperItems 数组中注册组件 ID(这里是 ‘upper’)
  • 视频播放组件的 id 设为 ‘upper’,就会被移到上半屏
  • 其他组件没有被 upperItems 修饰,就会显示在下半屏并自动避让折痕区

四、FoldSplitContainer 实现:固定分栏布局

实现原理

FoldSplitContainer 是系统提供的分栏类型的 ArkTS 组件,可以实现折叠屏二分栏、三分栏在展开态、悬停态以及折叠态的区域控制。

  • 二分栏:上下分栏
  • 三分栏:在二分栏基础上加上侧边栏

FoldSplitContainer 的参数:

  • primary:设置二分栏的上区域布局
  • secondary:设置二分栏的下区域布局
  • extra:设置三分栏中侧栏区域的布局
  • LayoutOptions:设置各区域分栏的比例

当设备进入悬停态时,FoldSplitContainer 会自动避让折叠屏折痕区。

适用场景

适用于分栏显示内容的场景,例如游戏画面和操作区域。

开发步骤

以游戏界面为例,将上下屏的组件分别注册到 primary 和 secondary 参数的回调中。

代码实现:

FoldSplitContainer({
  primary: () => {
    this.primaryArea(); // 上半屏内容
  },
  secondary: () => {
    this.secondaryArea(); // 下半屏内容
  }
})

这里只实现了二分栏结构,未实现 extra 参数对应的侧栏。


五、自定义实现:最灵活的方式

实现原理

自定义悬停态布局需要在折叠屏进入半折叠态时:

  • 设置窗口横向显示
  • 规避折痕避让区
  • 调整页面内组件的尺寸和位置

分为两部分:

  1. 监听悬停态:通过 display.on(‘foldStatusChange’) 接口监听设备是否进入半折叠态,同时通过 display 的 orientation 属性判断设备是否横屏,当两种状态都满足时即判断设备进入悬停态
  2. 调整布局:当设备进入悬停态后,通过 display.getCurrentFoldCreaseRegion() 接口获取折叠屏折痕区域的位置和大小,计算并设置上下半屏组件的尺寸和位置完成悬停态布局

注意:在退出应用或者退出需要监听折叠态变化的页面时,需要调用 display.off(‘foldStatusChange’) 接口取消监听,避免出现意想不到的问题。

适用场景

适用于页面布局复杂和悬停态触发动作自定义的场景。

开发步骤

1. 监听悬停态

悬停态通过状态变量 isHover 进行监听。当折叠屏的折叠状态变化时,判断当前是否为悬停态并更新 isHover 的值。

定义监听折叠状态变化回调方法:

private onFoldStatusChange: Callback<display.FoldStatus> = (data: display.FoldStatus) => {
  try {
    let orientation: display.Orientation = display.getDefaultDisplaySync().orientation;

    if (this.pageID === 0 || this.pageID === 3) {
      if (data === display.FoldStatus.FOLD_STATUS_HALF_FOLDED && 
          this.currentWidthBreakpoint === Const.BREAKPOINT_MD &&
          (orientation === display.Orientation.LANDSCAPE ||
           orientation === display.Orientation.LANDSCAPE_INVERTED)) {
        this.isHover = true;
        // ...
      } else {
        this.isHover = false;
      }
    }
  } catch (error) {
    hilog.error(0x0000, TAG, `onFoldStatusChange catch error, code: ${error.code}, message: ${error.message}`);
  }
};

判断逻辑:

  • FoldStatus 是 FOLD_STATUS_HALF_FOLDED(半折叠态)
  • 横向断点是 BREAKPOINT_MD
  • orientation 是 LANDSCAPE 或 LANDSCAPE_INVERTED(横屏)

三个条件都满足,才判定为悬停态。

在 display 中注册方法,监听设备折叠状态变化:

try {
  display.on('foldStatusChange', this.onFoldStatusChange);
} catch (exception) {
  hilog.error(0x0000, TAG, 'Failed to register onFoldStatusChange callback. Code: ' + JSON.stringify(exception));
}
2. 获取折痕区信息

当设备处于悬停状态(isHover 为 true)时,页面内组件需要获取折痕区的大小和位置:

static getFoldCreaseRegion(): void {
  try {
    if (display.isFoldable()) {
      let foldRegion: display.FoldCreaseRegion = display.getCurrentFoldCreaseRegion();
      let rect: display.Rect = foldRegion.creaseRects[0];
      // 折痕区上边界位置和折痕区高度
      let creaseRegion: number[] = [uiContext!.px2vp(rect.top), uiContext!.px2vp(rect.height)];
      AppStorage.setOrCreate('creaseRegion', creaseRegion);
    }
  } catch (error) {
    hilog.error(0x0000, TAG, `getFoldCreaseRegion catch error, code: ${error.code}, message: ${error.message}`);
  }
}

返回的 creaseRegion 数组:

  • creaseRegion[0]:折痕区上边界位置(上半屏高度)
  • creaseRegion[1]:折痕区高度(需要避让的区域高度)
3. 调整组件布局

根据折痕区的大小和位置调整布局:

视频播放组件:将上移至屏幕上方

Column() {
  XComponent({
    id: Const.X_COMPONENT_ID,
    type: XComponentType.SURFACE,
    controller: this.xComponentController
  })
    // ...
}
.height(this.isHover ? this.creaseRegion[0] : '100%')

悬停态时,视频播放组件高度设为折痕区上边界位置,刚好填满上半屏。

视频控制组件:位于下半屏,无需调整

顶部返回组件:移动到屏幕下半部分的顶部

Row() {
  // ...
}
.width('80%')
.height('24vp')
.justifyContent(FlexAlign.Start)
.position({
  x: '24vp',
  y: this.isHover ? this.creaseRegion[0] + this.creaseRegion[1] + 36 : '36vp'
})

悬停态时,返回组件的 y 坐标设为折痕区上边界 + 折痕区高度 + 36vp,刚好显示在下半屏顶部,避开了折痕区。


六、三种方式选用建议

场景 推荐方式
视频全屏播放,交互少 FolderStack
游戏界面,画面和操作分栏显示 FoldSplitContainer
页面布局复杂,需要自定义悬停触发条件 自定义实现
需要限制悬停态场景(如仅横屏半折叠) 自定义实现
需要自定义窗口旋转策略 自定义实现

简单场景用系统组件:

  • FolderStack:最简单,只需设置 upperItems
  • FoldSplitContainer:固定分栏布局,适合游戏

复杂场景自定义实现:

  • 可以限制悬停触发条件
  • 可以自定义窗口旋转策略
  • 但需要监听设备状态,开发难度大

七、踩坑记录

坑 1:FolderStack 必须撑满全屏

FolderStack 需要撑满页面全屏,如果不撑满页面全屏,则只作为普通 Stack 使用,悬停态不生效。

设置 FolderStack 的宽高:

FolderStack({ upperItems: ['upper'] }) {
  // ...
}
.width('100%')
.height('100%')

坑 2:FoldSplitContainer 的布局限制

FoldSplitContainer 只支持固定的二分栏和三分栏布局:

  • 不能自定义布局比例(只能通过 LayoutOptions 设置预设比例)
  • 不能自定义区域数量

如果需要自定义布局,就得用自定义实现。

坑 3:自定义实现要取消监听

在退出应用或者退出需要监听折叠态变化的页面时,必须调用 display.off(‘foldStatusChange’) 取消监听。

不取消的话,会继续监听设备状态变化,可能导致意外的问题。

取消监听:

display.off('foldStatusChange', this.onFoldStatusChange);

坑 4:判断悬停态要同时满足多个条件

悬停态判断要同时满足:

  • FoldStatus 是 FOLD_STATUS_HALF_FOLDED
  • 横向断点是 BREAKPOINT_MD
  • orientation 是 LANDSCAPE 或 LANDSCAPE_INVERTED

只判断 FoldStatus 不够,因为设备可能在竖屏下半折叠,此时不是悬停态。

坑 5:折痕区高度要用 px2vp 转换

display.getCurrentFoldCreaseRegion() 返回的尺寸单位是 px,需要用 px2vp 转换为 vp。

直接用 px 的话,布局尺寸会不对。

转换:

let creaseRegion: number[] = [uiContext!.px2vp(rect.top), uiContext!.px2vp(rect.height)];

八、总结

折叠屏悬停态适配有三种方式:

  • FolderStack:最简单,通过 upperItems 指定上半屏组件,其他组件自动显示在下半屏并避让折痕区
  • FoldSplitContainer:固定分栏布局,适合游戏画面和操作区域分栏
  • 自定义实现:最灵活,可以自定义悬停触发条件和窗口旋转策略,但开发难度大

简单场景用系统组件,复杂场景自定义实现。

关键是理解悬停态的特点:上半屏显示内容,下半屏显示控制,中间折痕区要避让。

掌握了这三种方式,折叠屏悬停态适配就不是啥难事了。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐