HarmonyOS 7 折叠屏平行视界的重构之路

本文复盘HarmonyOS 7折叠屏应用中平行视界功能的重构过程,重点解决仅监听屏幕宽度导致的布局失效、手动分栏引发的组件重建与状态丢失问题,以及弹窗定位和软键盘遮挡等交互陷阱。通过采用ArkUI原生SplitLayout组件、断点监听机制及键盘避让配置,实现大屏分栏与小屏压栈的自适应路由管理,强调避免硬编码尺寸、善用声明式布局与生命周期规范。

写在前面的碎碎念:

上周五,QA 拿着新买的折叠屏手机跑过来,直接甩我脸上:“你自己看看,你这页面展开之后像个大号直板机就算了,点个列表进去,详情页怎么把我列表给挤没了?”

我拿过来一看,确实惨不忍睹。之前做 UI 的时候,我习惯性地用了 width('100%') 和绝对定位,在直板机上看着岁月静好,一到折叠屏展开态(大屏),元素全被拉长;更致命的是,我用了简单的 if/else 来控制列表和详情的显示,展开后逻辑直接错乱。

这周末痛定思痛,把整个页面的折叠屏适配和平行视界重写了一遍。这篇文章就当是个复盘日记,记录一下我是怎么从一个只知道写死宽度的“小白”,一步步重构出能自适应大屏的架构的。

一开始,我天真地以为只需要监听宽度

刚接手这个需求时,我寻思着折叠屏不就是屏幕变宽了吗?那我在生命周期里拿一下屏幕宽度,做个判断不就行了?

这是我第一版极其简陋的代码:

typescript

import display from '@ohos.display';

@Entry
@Component
struct GoodsPage {
  @State isLargeScreen: boolean = false;

  aboutToAppear() {
    // 第一版:直接去拿当前屏幕宽度
    const screenWidth = display.getDefaultDisplaySync().width;
    // 假设 px 超过 800 算大屏
    this.isLargeScreen = px2vp(screenWidth) > 800;
  }

  build() {
    Row() {
      // 如果是大屏,左右分栏;如果是小屏,只显示列表
      if (this.isLargeScreen) {
        ListView().width('40%')
        DetailView().width('60%')
      } else {
        ListView().width('100%')
      }
    }
  }
}

折叠屏折叠/展开时,aboutToAppear 根本不会重新执行。 这意味着用户如果是在应用内把手机展开,我的页面依然是直板机布局,宽宽的大屏中间挤着一条窄窄的列表,堪称灾难。

后来我去查文档,发现不能这么玩,得用 mediaquery 去监听断点变化。改造后:

typescript

import mediaquery from '@ohos.mediaquery';

@Entry
@Component
struct GoodsPage {
  @State isLargeScreen: boolean = false;
  // 监听宽度大于 840vp 的断点
  private listener: mediaquery.MediaQueryListener = mediaquery.matchMediaSync('(width>=840vp)');

  aboutToAppear() {
    // 一开始先获取一次
    this.isLargeScreen = this.listener.matches;
    
    // 关键:监听折叠状态变化
    this.listener.on('change', (result: mediaquery.MediaQueryResult) => {
      this.isLargeScreen = result.matches;
    });
  }

  aboutToDisappear() {
    // 极其重要:组件销毁时一定要注销监听,否则会内存泄漏
    this.listener.off('change');
  }
  // build 代码略...
}

这样改完,终于能做到随着折叠状态动态切换布局了。

https://placeholder-01.png/

平行视界不是简单的左右分栏

布局跑通后,我开始做“平行视界”。产品经理的需求是:展开状态下,左边看列表,右边看详情。

我一开始想得很简单,用两个 Column 拼在一起,点击列表的某项,改变右边 Column 里的状态变量不就行了?

代码大概长这样:

typescript

@State currentDetailId: string = '';

// 在列表的点击事件里
.onClick(() => {
  this.currentDetailId = item.id;
})

// 右侧详情区
if (this.isLargeScreen) {
  Column() {
    if (this.currentDetailId) {
      DetailView({ id: this.currentDetailId })
    }
  }.width('60%')
}

这又掉进大坑了。 因为右侧的 DetailView 是随着 currentDetailId 变化的。每次点击,右侧组件都会经历一次完整的“销毁 -> 重建”过程。原本在详情页里滚动的进度、填写的表单,全丢了。更糟的是,如果用户点击返回键,我是该退回列表,还是关闭整个页面?

真正标准的做法,是用 ArkUI 提供的 Navigation 组件。它不仅原生支持分栏,还完美接管了返回栈。

重构后的核心结构:

typescript

@Provide('pageStack') pageStack: NavPathStack = new NavPathStack();

build() {
  Navigation(this.pageStack) {
    ListView() // 这里放列表
  }
  .mode(this.isLargeScreen ? NavigationMode.Split : NavigationMode.Stack) // 核心!
  .navBarWidth(300) // 左侧列表宽度
  .hideTitleBar(true)
}

当点击列表项时,不再是去改状态变量,而是往路由栈里推一个详情页:

typescript

this.pageStack.pushPath({ name: 'DetailPage', param: item.id });

Navigation 的 Split 模式非常聪明。在大屏下,它自动把列表页和详情页左右排布;当你把手机折叠起来(变成小屏),它会自动变成单页压栈模式,详情页覆盖列表页,用户按返回键,完美回退。之前手动写状态管理的痛楚,全被它解决了。

弹窗与软键盘陷阱不断

大屏适配不仅是界面的变形,还有很多交互细节。

我的列表页里有一个“筛选”按钮,点击后弹出一个自定义的 Dialog。在直板机上没问题,但在折叠屏展开时,因为屏幕极宽,这个 Dialog 如果依然居中,会和左右两边的列表、详情重叠,极其难看。

解决方法:给弹窗设置避让区域。

typescript

// 大屏下,把弹窗限制在列表所在的左侧区域弹出
promptAction.openCustomDialog({
  builder: () => { FilterDialog() },
  alignment: DialogAlignment.Bottom,
  // 动态计算偏移量
  offset: { dx: this.isLargeScreen ? -200 : 0, dy: 0 }
})

另外一个致命 bug 是软键盘。在详情页(分栏右侧)写评论时,键盘一弹出来,直接把整个页面往上顶,左侧的列表也全跟着往上移,导致用户不知道自己在哪里。

后来加上了键盘避让的配置:

typescript

// 在 EntryAbility 的 onWindowStageCreate 中
windowStage.getMainWindow().then((win) => {
  win.setWindowLayoutFullScreen(true); // 沉浸式
  // 设置键盘避让模式为 RESIZE,只有当前窗口会被压缩,不会整个页面平移
  win.getUIContext().setKeyboardAvoidMode(KeyboardAvoidMode.RESIZE);
});

最后经过反复测试,才勉强达到了及格线。真心建议各位,折叠屏测试一定要用真机,DevEco 的模拟器在某些手势和键盘交互上,和真机还是有差异。

https://placeholder-02.jpg/

经验总结与思考

这次折叠屏改造,折腾了我整整三天。总结几条血泪教训:

  1. 彻底戒掉写死尺寸的习惯。多用 layoutWeight 和百分比,绝对不要写 width(400)。

  2. Navigation 组件是神仙。不要试图用 if/else 去模拟多端布局,不仅逻辑混乱,还会带来严重的生命周期问题。

  3. 监听器一定要对生命负责。mediaquery 的 on 对应的一定要有 off。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值