折叠屏状态监听:横竖屏、折叠态差异化布局开发

折叠屏手机越来越多,普通应用如果不适配,展开大屏之后两边留一大块空白,或者布局被拉伸变形,体验很差。很多开发者一开始觉得折叠屏适配很复杂,要写两套布局、监听各种状态,实际上鸿蒙提供了完整的响应式布局能力,配合窗口状态监听,一套代码就能适配折叠、展开、横竖屏各种形态。

本文基于最新鸿蒙API,讲清楚折叠态怎么监听、响应式布局怎么写、横竖屏切换怎么处理,附带可直接运行的代码示例。

01 折叠屏设备状态概览

折叠屏手机有几种典型状态,应用要能识别并做对应布局:

  • 折叠态(合上):就是普通手机屏幕,单屏使用,宽度窄

  • 展开态(大屏):屏幕完全展开,平板级宽度,能显示更多内容

  • 悬停态:上半部分立着,下半部分像支架,一半显示内容一半放控制区

  • 横屏折叠:横着用,屏幕更长,适合看视频、文档

普通应用至少要适配折叠和展开两种状态。展开之后,不能只是把小屏布局放大拉伸,而是要利用大屏优势:列表页从单列变双列,详情页主内容和侧边栏并排显示,视频页上半放视频下半放评论。

鸿蒙的响应式布局是基于断点的:宽度小于320vp是小屏,320到600是中屏,600以上是大屏。折叠态一般落在小屏区间,展开态落在大屏区间。同一套代码,根据断点自动切换布局,不用写两套。

02 折叠态与屏幕状态监听

要监听折叠状态变化,用window模块的窗口属性回调。用户把手机从折叠展开的那一刻,系统会触发回调,应用可以拿到当前窗口的尺寸和显示状态。

typescript 复制代码
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct FoldableDemo {
  @State currentWidth: number = 0;
  @State isFoldable: boolean = false;
  @State foldStatus: string = ''; // 折叠/展开/悬停

  aboutToAppear() {
    this.initWindowListener();
  }

  async initWindowListener() {
    const context = getContext(this) as common.UIAbilityContext;
    const win = await window.getLastWindow(context);

    // 获取当前窗口尺寸
    const properties = win.getWindowProperties();
    this.currentWidth = properties.windowRect.width;

    // 监听窗口大小变化(折叠展开、横竖屏切换都会触发)
    win.on('windowSizeChange', (size) => {
      this.currentWidth = size.width;
      console.info('窗口宽度变化:', size.width);
    });

    // 监听窗口状态变化
    win.on('windowStateChange', (state) => {
      console.info('窗口状态变化:', state);
    });
  }

  build() {
    Column() {
      Text(`当前屏幕宽度: ${this.currentWidth}vp`)
        .fontSize(20)
      // 根据宽度判断布局类型
      if (this.currentWidth < 600) {
        Text('小屏布局(折叠态)').fontSize(16).margin(10)
      } else {
        Text('大屏布局(展开态)').fontSize(16).margin(10)
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

折叠展开的本质是屏幕宽度变了。所以不用专门去查"是不是折叠屏",直接监听窗口宽度变化就行。宽度小就是折叠态,宽度大就是展开态,逻辑通用,普通手机横屏也能自动适配。

还有一个能力是获取设备的折叠属性,判断当前是不是折叠屏、处于什么折叠状态。这个用display模块的属性来获取。

typescript 复制代码
import { display } from '@kit.ArkUI';

function getFoldStatus() {
  const displayInfo = display.getDefaultDisplaySync();
  // 获取折叠状态:0=展开, 1=折叠, 2=悬停
  const foldStatus = displayInfo.foldStatus;
  // 判断是否是折叠屏设备
  const isFoldable = displayInfo.isFoldable;

  console.info('是否折叠屏:', isFoldable);
  console.info('折叠状态:', foldStatus);
}

实际开发中,大部分场景不用特意判断是不是折叠屏,只要宽度变化了就重新布局。这样普通手机横竖屏切换、平板、折叠屏,一套代码全适配。

03 响应式布局基础:GridRow与断点

鸿蒙提供了GridRow栅格布局容器,专门用来做响应式页面。它会根据屏幕宽度自动切换列数,不用自己写if else判断。

typescript 复制代码
@Entry
@Component
struct ResponsiveLayout {
  build() {
    Column() {
      // 栅格布局,自动适配宽度
      GridRow({
        columns: {
          xs: 4,   // 小屏(<320vp):4列
          sm: 6,   // 中屏(320-600vp):6列
          md: 8,   // 大屏(600-840vp):8列
          lg: 12   // 超大屏(>840vp):12列
        },
        gutter: 12
      }) {
        // 每个GridCol指定占多少列
        GridCol({ span: { xs: 4, sm: 3, md: 4, lg: 3 } }) {
          Text('卡片1')
            .width('100%')
            .height(120)
            .backgroundColor('#0066FF')
        }
        GridCol({ span: { xs: 4, sm: 3, md: 4, lg: 3 } }) {
          Text('卡片2')
            .width('100%')
            .height(120)
            .backgroundColor('#3399FF')
        }
        GridCol({ span: { xs: 4, sm: 6, md: 8, lg: 6 } }) {
          Text('大卡片')
            .width('100%')
            .height(120)
            .backgroundColor('#66BBFF')
        }
      }
      .width('100%')
    }
    .width('100%')
    .padding(12)
  }
}

断点的四个档位:

  • xs:超小屏,宽度小于320vp,折叠态外屏一般是这个

  • sm:小屏,320到600vp,普通手机竖屏

  • md:中屏,600到840vp,折叠展开态、小平板

  • lg:大屏,大于840vp,大平板、横屏折叠

GridCol的span属性,每个断点占几列自己定。小屏的时候每个卡片占满4列,单列排列;大屏的时候一行放好几个,自动变成多列。这就是响应式布局的核心------一套代码,不同宽度自动调整列数。

04 折叠态差异化布局实现

展开大屏之后,不能只是把小屏布局拉伸,要做差异化设计。最典型的场景是列表加详情:小屏的时候点列表进详情页,跳一个新页面;大屏的时候,左边列表右边详情,一页显示两个。

typescript 复制代码
@Entry
@Component
struct ListDetailPage {
  @State selectedIndex: number = 0;
  @State screenWidth: number = 0;

  // 模拟数据
  private listData: string[] = ['消息1', '消息2', '消息3', '消息4', '消息5'];

  aboutToAppear() {
    // 获取当前宽度
    this.screenWidth = display.getDefaultDisplaySync().width;
  }

  build() {
    Row() {
      // 左边列表,所有状态都显示
      Column() {
        ForEach(this.listData, (item: string, index: number) => {
          Text(item)
            .width('100%')
            .height(60)
            .padding(16)
            .backgroundColor(this.selectedIndex === index ? '#E6F0FF' : '#FFFFFF')
            .onClick(() => {
              this.selectedIndex = index;
            })
        })
      }
      .width(this.screenWidth < 600 ? '100%' : '30%')
      .border({ right: { width: 1, color: '#EEEEEE' } })

      // 右边详情,只有大屏才显示
      if (this.screenWidth >= 600) {
        Column() {
          Text('详情内容')
            .fontSize(24)
          Text(`当前选中: ${this.listData[this.selectedIndex]}`)
            .fontSize(16)
            .margin({ top: 20 })
        }
        .layoutWeight(1)
        .padding(20)
      }
    }
    .width('100%')
    .height('100%')
  }
}

这个例子里:

  • 折叠态(小屏):只显示列表,点一条消息跳新页面看详情,和普通手机一样

  • 展开态(大屏):左边列表右边详情,选左边右边同步更新,不用跳页面

这种列表+详情的双栏布局,是折叠屏适配最常见的模式。邮箱、设置页、消息列表,都适合这么做。大屏不浪费空间,小屏也不影响使用。

判断宽度的地方,最好封装成一个状态变量,不要到处写if。窗口变化的时候自动更新状态,UI自动刷新。

05 横竖屏切换适配

折叠屏用户经常会转屏幕看视频、看文档。横竖屏切换的时候,布局也要跟着调整,不能竖屏是竖的、横屏还是一样的布局拉伸。

默认情况下,鸿蒙应用是跟随屏幕自动旋转的。但有些应用只允许竖屏,这时候要在module.json5里配置:

json 复制代码
"abilities": [
  {
    "name": "EntryAbility",
    "export": true,
    "orientation": "unspecified"
  }
]

orientation几个常用值:

  • unspecified:跟随系统自动旋转,推荐大多数应用

  • portrait:只竖屏,强制不能转横屏

  • landscape:只横屏,比如视频播放页

如果要跟随旋转,布局里就要考虑横屏的情况。横屏之后屏幕高度变矮、宽度变宽,原来上下排列的内容,可能要改成左右排列。这个和折叠屏展开本质是一样的,都是宽度变了,用断点响应式布局自动适配就行。

还有一个细节:横竖屏切换的时候,页面状态要保留。用户竖屏输入了一半内容,转横屏不能清空重新来。ArkUI的状态管理会自动保存这些状态,不用自己处理。

06 完整页面适配示例

把窗口监听、响应式布局、差异化布局整合到一起,就是一个完整的折叠屏适配页面。

typescript 复制代码
import { window } from '@kit.ArkUI';
import { display } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct FoldablePage {
  @State breakPoint: string = 'sm'; // 当前断点档位
  @State list: string[] = ['商品1', '商品2', '商品3', '商品4', '商品5'];
  @State selectedId: number = 0;

  aboutToAppear() {
    this.updateBreakPoint();
    this.listenWindowChange();
  }

  // 根据宽度更新断点
  updateBreakPoint() {
    const width = display.getDefaultDisplaySync().width;
    if (width < 320) {
      this.breakPoint = 'xs';
    } else if (width < 600) {
      this.breakPoint = 'sm';
    } else if (width < 840) {
      this.breakPoint = 'md';
    } else {
      this.breakPoint = 'lg';
    }
  }

  // 监听窗口大小变化
  async listenWindowChange() {
    const ctx = getContext(this) as common.UIAbilityContext;
    const win = await window.getLastWindow(ctx);
    win.on('windowSizeChange', () => {
      this.updateBreakPoint();
    });
  }

  build() {
    if (this.breakPoint === 'sm' || this.breakPoint === 'xs') {
      // 小屏:单列列表,点了跳详情
      this.SmallLayout()
    } else {
      // 大屏:左右分栏,列表+详情
      this.LargeLayout()
    }
  }

  @Builder
  SmallLayout() {
    Column() {
      ForEach(this.list, (item: string, index: number) => {
        Text(item)
          .width('100%')
          .height(80)
          .padding(20)
          .border({ bottom: { width: 1, color: '#EEEEEE' } })
          .onClick(() => {
            this.selectedId = index;
          })
      })
    }
    .width('100%')
  }

  @Builder
  LargeLayout() {
    Row() {
      Column() {
        ForEach(this.list, (item: string, index: number) => {
          Text(item)
            .width('100%')
            .height(80)
            .padding(20)
            .backgroundColor(this.selectedId === index ? '#E6F0FF' : '#FFFFFF')
            .onClick(() => {
              this.selectedId = index;
            })
        })
      }
      .width('35%')

      Column() {
        Text('商品详情')
          .fontSize(28)
        Text(this.list[this.selectedId])
          .fontSize(20)
          .margin({ top: 30 })
      }
      .layoutWeight(1)
      .padding(30)
      .backgroundColor('#F8F9FA')
    }
    .width('100%')
    .height('100%')
  }
}

这个页面的逻辑很简单:根据当前断点档位,选择小屏布局还是大屏布局。窗口宽度变了,自动更新断点,UI自动切换。折叠展开、横竖屏切换,都自动适配。

落地注意事项

**不要硬编码宽度。**不要写死"小于360就是手机、大于600就是平板",不同设备宽度不一样。用系统提供的断点档位,不要自己定数值。

**大屏不要只是拉伸。**很多人适配折叠屏的做法是把小屏布局放大,两边留空白。这样大屏体验很差,要利用大屏优势做双栏、多列、侧边栏。

**状态要保留。**折叠展开切换布局的时候,用户选了哪个列表项、输入了多少内容,这些状态要保留。不能一展开就变成初始状态。

**弹窗位置要注意。**大屏的时候弹窗居中显示,不要贴在一边,利用好大屏空间。

小结

折叠屏适配的核心不是监听各种折叠状态,而是做响应式布局。监听窗口宽度变化,根据断点自动切换布局。折叠展开、横竖屏切换、平板设备,本质都是屏幕宽度变了,一套响应式代码全适配。

实际开发中,先把核心列表页、详情页做好大小屏两种布局,其他页面逐步跟进。不用一开始就全部页面都适配,先覆盖主流程,用户体验就已经好很多了。

相关推荐
2501_944525541 小时前
开源鸿蒙平台 KMP 三方库「FileKit」适配全流程
华为·开源·harmonyos
OH_TPC1 小时前
HarmonyOS APP开发---“云诊室“在线问诊App,需要用到这个库
华为·harmonyos·鸿蒙·1024程序员节
鸿蒙开发14 小时前
共享 Schema 与 SchemaFactory
前端·后端·harmonyos
二流小码农16 小时前
HarmonyOS开发助手,0代码让元服务从开发到上架
android·ios·harmonyos
产品人卫朋17 小时前
MTL如何把市场机会转化为有效销售线索?
人工智能·华为·产品经理·ipd流程·流程架构·mtl流程
贾伟康18 小时前
【共创稿事节】第一张服务卡片:2×2 今日菜谱的克制设计
microsoft·harmonyos·arkts·鸿蒙
2501_9445257618 小时前
开源鸿蒙平台 KMP_CMP 三方库「Atrium」适配全流程
microsoft·开源·harmonyos
less_1213818 小时前
鸿蒙 WPS Open SDK 二开复盘:从注册、打开到回传的完整能力链
服务器·sdk·harmonyos·wps·鸿蒙开发
2601_9495758619 小时前
开源鸿蒙平台 CMP 三方库「voyager」适配全流程
华为·开源·harmonyos