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

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

核心概念
| 折叠状态 | 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 逻辑
更多推荐



所有评论(0)