前言

折叠屏是 HarmonyOS 生态的重要设备形态。展开时屏幕宽度接近平板,适合双栏布局(左列表 + 右详情);折叠时退化为手机形态,需要切换为单栏。本文用 display API 监听折叠状态,配合 Navigation 实现真正自适应的折叠屏布局。

运行效果

初始状态(展开态,双栏布局)

idle

切换至折叠态(单栏布局)

done

核心概念

折叠状态 display.FoldStatus 推荐布局
展开 EXPANDED 2 双栏:左侧列表 + 右侧详情
半折 HALF_FOLDED 3 上半屏内容 + 下半屏操作
折叠 FOLDED 1 单栏:全屏列表,点击跳转详情

完整示例代码

interface ArticleItem {
  id: number
  icon: string
  title: string
  summary: string
  tag: string
  readTime: string
}

@Entry
@Component
struct Index {
  // 模拟折叠状态:'expanded' | 'folded' | 'halfFolded'
  @State foldState: string = 'expanded'
  @State selectedArticleId: number = 1

  private articles: ArticleItem[] = [
    { id: 1, icon: '📐', tag: '布局', title: 'Stack 层叠布局与 Overlay 浮层详解', summary: '深入理解 alignContent 九宫格、zIndex 层叠顺序与 overlay 实战...', readTime: '8分钟' },
    { id: 2, icon: '📋', tag: '列表', title: 'List 高性能长列表与分组吸顶', summary: '虚拟化渲染原理、ListItemGroup 分组、sticky 吸顶完整实现...', readTime: '10分钟' },
    { id: 3, icon: '🎠', tag: '轮播', title: 'Swiper 轮播图与页面滑动指南', summary: 'DotIndicator 自定义、SwiperController 编程翻页、卡片流效果...', readTime: '7分钟' },
    { id: 4, icon: '📑', tag: '导航', title: 'Tabs 标签页:底部导航到顶部分类', summary: '底部导航栏、嵌套 Tabs、自定义 TabBar 样式完整示例...', readTime: '9分钟' },
    { id: 5, icon: '🗂', tag: '侧栏', title: 'SideBarContainer 侧边导航实战', summary: 'Embed vs Overlay 模式、autoHide 自适应、自定义汉堡菜单...', readTime: '11分钟' },
  ]

  private getSelectedArticle(): ArticleItem {
    for (let i = 0; i < this.articles.length; i++) {
      if (this.articles[i].id === this.selectedArticleId) {
        return this.articles[i]
      }
    }
    return this.articles[0]
  }

  @Builder
  foldStateSelector() {
    Column({ space: 12 }) {
      Text('模拟折叠状态:')
        .fontSize(13)
        .fontColor('#888')
        .width('100%')
        .textAlign(TextAlign.Center)

      Row({ space: 8 }) {
        Button('展开').height(36).fontSize(13)
          .backgroundColor(this.foldState === 'expanded' ? '#0066ff' : '#e0e0e0')
          .fontColor(this.foldState === 'expanded' ? '#fff' : '#333')
          .onClick(() => { this.foldState = 'expanded' })

        Button('半折').height(36).fontSize(13)
          .backgroundColor(this.foldState === 'halfFolded' ? '#ff8800' : '#e0e0e0')
          .fontColor(this.foldState === 'halfFolded' ? '#fff' : '#333')
          .onClick(() => { this.foldState = 'halfFolded' })

        Button('折叠').height(36).fontSize(13)
          .backgroundColor(this.foldState === 'folded' ? '#cc3300' : '#e0e0e0')
          .fontColor(this.foldState === 'folded' ? '#fff' : '#333')
          .onClick(() => { this.foldState = 'folded' })
      }
      .justifyContent(FlexAlign.Center)

      Text(
        this.foldState === 'expanded' ? '📱 展开态:双栏布局,左列表 + 右详情' :
        this.foldState === 'halfFolded' ? '🔄 半折态:上内容 + 下操作区' :
        '📱 折叠态:单栏,仅显示列表'
      )
        .fontSize(12)
        .fontColor(
          this.foldState === 'expanded' ? '#0066ff' :
          this.foldState === 'halfFolded' ? '#ff8800' : '#cc3300'
        )
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .padding({ top: 12, bottom: 12, left: 16, right: 16 })
    .backgroundColor(
      this.foldState === 'expanded' ? '#f0f5ff' :
      this.foldState === 'halfFolded' ? '#fff8f0' : '#fff0f0'
    )
    .border({ width: { bottom: 1 }, color: '#f0f0f0' })
  }

  @Builder
  articleList() {
    Column({ space: 0 }) {
      Row() {
        Text('📚 文章列表').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#1a1a1a')
        Text(this.articles.length.toString() + ' 篇').fontSize(13).fontColor('#888')
      }
      .width('100%')
      .height(48)
      .padding({ left: 16, right: 16 })
      .border({ width: { bottom: 1 }, color: '#eeeeee' })

      ForEach(this.articles, (article: ArticleItem) => {
        Row() {
          Text(article.icon).fontSize(20).width(36)
          Column({ space: 4 }) {
            Text(article.title)
              .fontSize(13)
              .fontColor(this.selectedArticleId === article.id ? '#0066ff' : '#222')
              .fontWeight(this.selectedArticleId === article.id ? FontWeight.Bold : FontWeight.Normal)
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
            Row({ space: 6 }) {
              Text(article.tag)
                .fontSize(10).fontColor('#0066ff').backgroundColor('#f0f5ff')
                .padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(4)
              Text(article.readTime).fontSize(10).fontColor('#aaa')
            }
          }
          .layoutWeight(1).alignItems(HorizontalAlign.Start).padding({ left: 8 })
        }
        .width('100%')
        .padding({ left: 12, right: 12, top: 12, bottom: 12 })
        .backgroundColor(this.selectedArticleId === article.id ? '#f0f5ff' : '#ffffff')
        .border({
          width: { left: this.selectedArticleId === article.id ? 3 : 0, bottom: 1 },
          color: { left: '#0066ff', bottom: '#f5f5f5' }
        })
        .onClick(() => {
          this.selectedArticleId = article.id
        })
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ffffff')
  }

  // @Builder 内不允许 let/const 变量声明,用 this.getSelectedArticle() 直接调用
  @Builder
  articleDetailPanel() {
    Scroll() {
      Column({ space: 20 }) {
        Column({ space: 12 }) {
          Row({ space: 12 }) {
            Text(this.getSelectedArticle().icon).fontSize(36)
            Column({ space: 4 }) {
              Text(this.getSelectedArticle().tag)
                .fontSize(11).fontColor('#0066ff').backgroundColor('#f0f5ff')
                .padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
              Text(this.getSelectedArticle().readTime + ' 阅读').fontSize(12).fontColor('#aaa')
            }
            .alignItems(HorizontalAlign.Start)
          }
          Text(this.getSelectedArticle().title)
            .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#111').lineHeight(28)
          Text(this.getSelectedArticle().summary)
            .fontSize(14).fontColor('#666').lineHeight(22)
        }
        .width('100%').padding(20)
        .backgroundColor('#ffffff').borderRadius(12)
        .border({ width: 1, color: '#f0f0f0' })

        Column({ space: 12 }) {
          Text('正文内容').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333')
          Text('HarmonyOS 的折叠屏适配是现代应用开发的重要能力。通过监听屏幕折叠状态,开发者可以动态调整布局策略,为用户提供最佳的视觉体验。')
            .fontSize(14).fontColor('#555').lineHeight(22)
          Text('在展开状态下,建议采用双栏布局:左栏展示列表,右栏展示详情,充分利用大屏幕空间;在折叠状态下,切换为单栏布局,保证内容可读性。')
            .fontSize(14).fontColor('#555').lineHeight(22)
        }
        .width('100%').padding(20)
        .backgroundColor('#ffffff').borderRadius(12)
        .border({ width: 1, color: '#f0f0f0' })
        .alignItems(HorizontalAlign.Start)

        // 真机 API 代码展示
        Column({ space: 10 }) {
          Text('真机折叠状态检测 API').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#333')
          Column({ space: 6 }) {
            Text("import display from '@ohos.display'").fontSize(11).fontColor('#00aa55').fontFamily('monospace')
            Text('').height(4)
            Text('let foldStatus = display.getFoldStatus()').fontSize(11).fontColor('#0066ff').fontFamily('monospace')
            Text('// FoldStatus.FOLD_STATUS_EXPANDED  = 2').fontSize(11).fontColor('#666').fontFamily('monospace')
            Text('// FoldStatus.FOLD_STATUS_FOLDED    = 1').fontSize(11).fontColor('#666').fontFamily('monospace')
            Text('').height(4)
            Text("display.on('foldStatusChange', (status) => {").fontSize(11).fontColor('#0066ff').fontFamily('monospace')
            Text('  // 动态切换布局').fontSize(11).fontColor('#666').fontFamily('monospace')
            Text('})').fontSize(11).fontColor('#0066ff').fontFamily('monospace')
          }
          .width('100%').padding(14).backgroundColor('#1a1a2e').borderRadius(10)
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%').padding(20)
        .backgroundColor('#ffffff').borderRadius(12)
        .border({ width: 1, color: '#f0f0f0' })
        .alignItems(HorizontalAlign.Start)

        if (this.foldState === 'halfFolded') {
          Column({ space: 8 }) {
            Text('⚠️ 半折状态提示').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#cc6600')
            Text('建议将交互控件放置在下半屏,将内容展示放置在上半屏,模拟笔记本电脑使用模式。')
              .fontSize(13).fontColor('#664400').lineHeight(20)
          }
          .width('100%').padding(16)
          .backgroundColor('#fff8e8').borderRadius(12)
          .border({ width: 1, color: '#ffcc66' })
          .alignItems(HorizontalAlign.Start)
        }

        Column().height(20)
      }
      .width('100%').padding({ left: 16, right: 16, top: 16, bottom: 16 })
    }
    .width('100%').height('100%').backgroundColor('#f5f5f5')
  }

  build() {
    Column({ space: 0 }) {
      // 顶部:折叠状态选择器
      this.foldStateSelector()

      // 主内容区:根据折叠状态切换布局
      if (this.foldState === 'expanded') {
        // 展开态:双栏布局
        Row() {
          Column() {
            this.articleList()
          }
          .width('40%')
          .height('100%')
          .border({ width: { right: 1 }, color: '#e8e8e8' })

          Column() {
            this.articleDetailPanel()
          }
          .layoutWeight(1)
          .height('100%')
        }
        .width('100%')
        .layoutWeight(1)

      } else if (this.foldState === 'folded') {
        // 折叠态:单栏列表
        Column() {
          this.articleList()
        }
        .width('100%')
        .layoutWeight(1)

      } else {
        // 半折态:上下分屏
        Column() {
          // 上半屏:内容
          Column() {
            this.articleDetailPanel()
          }
          .height('55%')
          .border({ width: { bottom: 2 }, color: '#0066ff' })

          // 下半屏:操作列表
          Column() {
            this.articleList()
          }
          .height('45%')
        }
        .width('100%')
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#f5f5f5')
  }
}

关键知识点

1. @Builder 内不允许 let/const 声明

这是 ArkTS 严格模式的重要限制:

// 错误:@Builder 内不能声明变量
@Builder
articleDetail() {
  let article = this.getSelected()  // 编译错误!
  Text(article.title)
}

// 正确:直接调用方法或访问 @State
@Builder
articleDetail() {
  Text(this.getSelected().title)    // 允许方法调用
  Text(this.selectedTitle)          // 允许访问 @State
}

2. 真机获取折叠状态

import display from '@ohos.display'

// 一次性获取
let status = display.getFoldStatus()
// FoldStatus.FOLD_STATUS_FOLDED       = 1 (折叠)
// FoldStatus.FOLD_STATUS_EXPANDED     = 2 (展开)
// FoldStatus.FOLD_STATUS_HALF_FOLDED  = 3 (半折)

// 持续监听
display.on('foldStatusChange', (status: display.FoldStatus) => {
  this.foldState = status === display.FoldStatus.FOLD_STATUS_EXPANDED ? 'expanded' : 'folded'
})

3. 三种布局对应三种状态

折叠状态 布局策略 实现方式
展开 双栏:列表 40% + 详情 60% Row 包裹两个 Column
折叠 单栏:全屏列表 单独 Column
半折 纵向分割:上内容 + 下操作 Column 包裹两个固定高度 Column

4. Navigation 的 Auto 模式更简洁

对于普通列表-详情场景,NavigationMode.Auto 自动根据屏幕宽度在 Stack(手机)和 Split(平板/折叠屏展开态)之间切换,无需手动判断折叠状态:

Navigation(this.pathStack)
  .mode(NavigationMode.Auto)  // 宽屏自动双栏,窄屏自动单栏

这是处理折叠屏最推荐的简洁方式。

5. 半折态的 UX 建议

半折时设备像一台笔记本:折痕以上是“屏幕“,折痕以下是“键盘区“。建议:

  • 上半屏:展示内容(文章、图片、视频)
  • 下半屏:放置交互控件(按钮、输入框、导航)

小结

  • @Builder 内不允许 let/const 变量声明,改为直接调用方法或引用 @State
  • 真机通过 display.getFoldStatus() 获取折叠状态,display.on('foldStatusChange') 持续监听
  • 三种折叠状态对应三种布局:展开双栏、折叠单栏、半折上下分屏
  • NavigationMode.Auto 是处理折叠屏列表-详情场景的最简洁方案,推荐优先使用
  • 模拟器无法真实模拟折叠屏,但可以通过 @State foldState 切换布局验证 UI 逻辑
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐