分栏布局是平板和折叠屏应用的基本功。手机上用栈式导航,平板上用左右分栏,折叠屏要能在两种模式间无缝切换。HarmonyOS的Navigation组件内置了Split/Stack/Auto三种模式,但真要用好,还得理解底层的适配逻辑。
Navigation三种模式
typescript
Navigation() {
// NavDestination内容
}
.mode(NavigationMode.Split) // 分栏模式
| 模式 | 行为 | 适用场景 |
|---|---|---|
| Split | 左侧导航栏+右侧内容区 | 平板、折叠屏展开 |
| Stack | 单栏栈式推入 | 手机 |
| Auto | 根据宽度自动选择 | 通用 |
Auto模式的阈值是600vp------宽度超过600vp自动切Split,低于则切Stack。这个阈值可以在Navigation的minContentWidth参数中调整。

Split模式详解
Split模式下Navigation会自动分成两栏:
typescript
Navigation() {
NavDestination() {
// 列表页(左栏)
}
NavDestination() {
// 详情页(右栏)
}
}
.mode(NavigationMode.Split)
.navBarWidth(240) // 左栏宽度
.navBarWidthRange([200, 360]) // 左栏宽度范围(可拖拽调整)
navBarWidth设置左栏宽度,默认240vp。navBarWidthRange设置拖拽调整的范围------用户可以手动调整左栏宽度。
左栏的NavDestination就是导航列表,右栏显示被点击的详情页。在Split模式下,push一个新NavDestination会在右栏显示,不会覆盖左栏。
Stack模式
Stack模式就是传统的手机导航:
typescript
Navigation() {
NavDestination() {
// 列表页
}
}
.mode(NavigationMode.Stack)
push新页面时整个屏幕推入,pop时退出。左栏不存在,所有页面共享整个屏幕。
折叠屏适配
折叠屏的核心问题是:折叠/展开时布局要跟着变。监听窗口尺寸变化:
typescript
import { display } from '@kit.ArkUI';
@Component
struct FoldablePage {
@State isExpanded: boolean = false
private mainWindow: window.Window = AppStorage.get<window.Window>('mainWindow')
aboutToAppear(): void {
this.mainWindow.on('windowSizeChange', (size: window.Size) => {
this.isExpanded = size.width > 600
})
}
build() {
Navigation() {
// 内容
}
.mode(this.isExpanded ? NavigationMode.Split : NavigationMode.Stack)
}
}
windowSizeChange回调在折叠/展开、旋转、分屏时都会触发。用width>600判断是否展开态,动态切换NavigationMode。
注意:NavigationMode切换会导致NavPathStack重置。 如果栈里有数据,切换模式前要保存状态,切换后恢复。更好的方式是一直用Auto模式,让系统自动处理。
Auto模式最佳实践
typescript
Navigation(this.navPathStack) {
// 首页
}
.mode(NavigationMode.Auto)
.navBarWidth(280)
.minContentWidth(360)
Auto模式避免了手动切换的麻烦。minContentWidth是右栏的最小宽度------当右栏宽度不足时自动切回Stack。设360vp保证详情页有足够空间。
Auto模式的切换时机:
- 宽度 >= navBarWidth + minContentWidth → Split
- 宽度 < navBarWidth + minContentWidth → Stack
自定义分栏
不用Navigation也能做分栏,用Row手动实现:
typescript
@Component
struct CustomSplitPage {
@State isSplit: boolean = true
@State selectedIndex: number = 0
build() {
if (this.isSplit) {
Row() {
// 左栏
Column() {
ForEach(this.menuItems, (item: MenuItem, index: number) => {
Row() {
Text(item.title)
.fontColor(this.selectedIndex === index ? '#007DFF' : '#333333')
}
.width('100%')
.padding(12)
.onClick(() => { this.selectedIndex = index })
}, (item: MenuItem, index: number) => index.toString())
}
.width('40%')
.height('100%')
.backgroundColor('#fafafa')
// 右栏
Column() {
Text(this.menuItems[this.selectedIndex].title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.layoutWeight(1)
.height('100%')
.backgroundColor('#ffffff')
}
.width('100%')
.height('100%')
} else {
// 单栏模式
Column() {
// 列表或详情
}
.width('100%')
.height('100%')
}
}
}
自定义分栏更灵活------可以控制左栏比例、动画过渡、分栏线拖拽。但需要自己处理页面栈逻辑。
主从列表模式
最经典的分栏场景:左侧列表、右侧详情。邮件、笔记、设置都是这个模式。
typescript
@Component
struct MasterDetailPage {
@State selectedItem: MailItem | undefined = undefined
@State isSplit: boolean = false
build() {
if (this.isSplit) {
Row() {
// 主列表
List() {
ForEach(this.mails, (mail: MailItem) => {
ListItem() {
MailRow({ mail: mail, isSelected: this.selectedItem?.id === mail.id })
.onClick(() => { this.selectedItem = mail })
}
}, (mail: MailItem) => mail.id)
}
.width('40%')
.height('100%')
// 详情
if (this.selectedItem !== undefined) {
MailDetail({ mail: this.selectedItem })
.layoutWeight(1)
} else {
Column() {
Text('选择一封邮件')
.fontColor('#999999')
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
}
} else {
// 手机:先显示列表,点击跳详情
List() {
ForEach(this.mails, (mail: MailItem) => {
ListItem() {
MailRow({ mail: mail })
.onClick(() => {
this.navPathStack.pushPath({ name: 'detail', param: mail })
})
}
}, (mail: MailItem) => mail.id)
}
}
}
}
selectedItem为undefined时右栏显示占位提示。这种空状态很重要------分栏模式下用户第一眼看到的是空详情区。
分栏比例调节
固定比例:
typescript
Row() {
Column() {}.width('35%') // 左栏
Column() {}.width('65%') // 右栏
}
可拖拽比例:
typescript
@Component
struct ResizableSplit {
@State leftWidth: number = 280
private minLeft: number = 200
private maxLeft: number = 400
build() {
Stack() {
Row() {
Column() {}.width(this.leftWidth)
Column() {}.layoutWeight(1)
}
// 拖拽手柄
Column() {}
.width(20)
.height('100%')
.backgroundColor(Color.Transparent)
.position({ x: this.leftWidth - 10, y: 0 })
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
let newWidth: number = this.leftWidth + event.offsetX
if (newWidth >= this.minLeft && newWidth <= this.maxLeft) {
this.leftWidth = newWidth
}
})
)
}
}
}
拖拽手柄是一个20vp宽的透明列,用PanGesture拖动改变leftWidth。position定位在分栏线位置。
窗口宽度监听
不用display API也能监听宽度------用onAreaChange:
typescript
Row() {
// 分栏内容
}
.onAreaChange((oldArea: Area, newArea: Area) => {
let width: number = Number(newArea.width)
this.isSplit = width > 600
})
onAreaChange在组件挂载和尺寸变化时都会触发。比display API更精确------因为组件宽度不一定等于屏幕宽度(考虑分屏、自由窗口)。

折叠屏状态获取
typescript
import { display } from '@kit.ArkUI';
let foldStatus: display.FoldStatus = display.getFoldStatusSync();
// FoldStatus.FOLD_STATUS_UNKNOWN
// FoldStatus.FOLD_STATUS_EXPANDED 展开
// FoldStatus.FOLD_STATUS_FOLDED 折叠
// FoldStatus.FOLL_STATUS_HALF_FOLDED 半折叠
getFoldStatusSync()只在折叠屏设备上返回有效值,普通设备返回UNKNOWN。
监听折叠状态变化:
typescript
display.on('foldStatusChange', (status: display.FoldStatus) => {
if (status === display.FoldStatus.FOLD_STATUS_EXPANDED) {
// 展开态 → 切Split
} else if (status === display.FoldStatus.FOLD_STATUS_FOLDED) {
// 折叠态 → 切Stack
}
})
踩坑清单
| 问题 | 原因 | 解决 |
|---|---|---|
| Split模式左栏太窄 | 默认navBarWidth=240 | 加大navBarWidth或设navBarWidthRange |
| 切换模式后栈数据丢失 | NavigationMode切换重置栈 | 用Auto模式自动切换,或手动保存栈 |
| 右栏空白无内容 | 没有选中项 | 显示占位提示"请选择" |
| 分屏时布局错乱 | 用屏幕宽度而非组件宽度 | 用onAreaChange监听组件宽度 |
| 拖拽手柄不响应 | PanGesture被父组件消费 | gesture用priorityGesture |
| 折叠屏检测无效 | 普通设备返回UNKNOWN | 先判断是否折叠屏再处理 |
| 左栏比例不协调 | 固定px在不同设备不一致 | 用百分比或vp单位 |
| Auto模式频繁切换 | minContentWidth太大 | 调小minContentWidth到320 |
| 详情页刷新闪烁 | selectedItem引用没变 | 切换时创建新对象触发刷新 |
| 分栏线遮挡内容 | position定位覆盖 | 分栏线区域设透明或调z序 |
分栏适配的核心就一句话:用Auto模式+onAreaChange,让布局跟着宽度走。不要硬编码模式切换,让系统根据空间自动决定。自定义分栏只在Navigation满足不了需求时才考虑。