分栏布局是平板和折叠屏应用的基本功。手机上用栈式导航,平板上用左右分栏,折叠屏要能在两种模式间无缝切换。HarmonyOS的Navigation组件内置了Split/Stack/Auto三种模式,但真要用好,还得理解底层的适配逻辑。
Navigation三种模式
Navigation(){// NavDestination内容}.mode(NavigationMode.Split)// 分栏模式| 模式 | 行为 | 适用场景 |
|---|---|---|
| Split | 左侧导航栏+右侧内容区 | 平板、折叠屏展开 |
| Stack | 单栏栈式推入 | 手机 |
| Auto | 根据宽度自动选择 | 通用 |
Auto模式的阈值是600vp——宽度超过600vp自动切Split,低于则切Stack。这个阈值可以在Navigation的minContentWidth参数中调整。
Split模式详解
Split模式下Navigation会自动分成两栏:
Navigation(){NavDestination(){// 列表页(左栏)}NavDestination(){// 详情页(右栏)}}.mode(NavigationMode.Split).navBarWidth(240)// 左栏宽度.navBarWidthRange([200,360])// 左栏宽度范围(可拖拽调整)navBarWidth设置左栏宽度,默认240vp。navBarWidthRange设置拖拽调整的范围——用户可以手动调整左栏宽度。
左栏的NavDestination就是导航列表,右栏显示被点击的详情页。在Split模式下,push一个新NavDestination会在右栏显示,不会覆盖左栏。
Stack模式
Stack模式就是传统的手机导航:
Navigation(){NavDestination(){// 列表页}}.mode(NavigationMode.Stack)push新页面时整个屏幕推入,pop时退出。左栏不存在,所有页面共享整个屏幕。
折叠屏适配
折叠屏的核心问题是:折叠/展开时布局要跟着变。监听窗口尺寸变化:
import{display}from'@kit.ArkUI';@Componentstruct FoldablePage{@StateisExpanded:boolean=falseprivatemainWindow: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模式最佳实践
Navigation(this.navPathStack){// 首页}.mode(NavigationMode.Auto).navBarWidth(280).minContentWidth(360)Auto模式避免了手动切换的麻烦。minContentWidth是右栏的最小宽度——当右栏宽度不足时自动切回Stack。设360vp保证详情页有足够空间。
Auto模式的切换时机:
- 宽度 >= navBarWidth + minContentWidth → Split
- 宽度 < navBarWidth + minContentWidth → Stack
自定义分栏
不用Navigation也能做分栏,用Row手动实现:
@Componentstruct CustomSplitPage{@StateisSplit:boolean=true@StateselectedIndex:number=0build(){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%')}}}自定义分栏更灵活——可以控制左栏比例、动画过渡、分栏线拖拽。但需要自己处理页面栈逻辑。
主从列表模式
最经典的分栏场景:左侧列表、右侧详情。邮件、笔记、设置都是这个模式。
@Componentstruct MasterDetailPage{@StateselectedItem:MailItem|undefined=undefined@StateisSplit:boolean=falsebuild(){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时右栏显示占位提示。这种空状态很重要——分栏模式下用户第一眼看到的是空详情区。
分栏比例调节
固定比例:
Row(){Column(){}.width('35%')// 左栏Column(){}.width('65%')// 右栏}可拖拽比例:
@Componentstruct ResizableSplit{@StateleftWidth:number=280privateminLeft:number=200privatemaxLeft:number=400build(){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)=>{letnewWidth:number=this.leftWidth+event.offsetXif(newWidth>=this.minLeft&&newWidth<=this.maxLeft){this.leftWidth=newWidth}}))}}}拖拽手柄是一个20vp宽的透明列,用PanGesture拖动改变leftWidth。position定位在分栏线位置。
窗口宽度监听
不用display API也能监听宽度——用onAreaChange:
Row(){// 分栏内容}.onAreaChange((oldArea:Area,newArea:Area)=>{letwidth:number=Number(newArea.width)this.isSplit=width>600})onAreaChange在组件挂载和尺寸变化时都会触发。比display API更精确——因为组件宽度不一定等于屏幕宽度(考虑分屏、自由窗口)。
折叠屏状态获取
import{display}from'@kit.ArkUI';letfoldStatus:display.FoldStatus=display.getFoldStatusSync();// FoldStatus.FOLD_STATUS_UNKNOWN// FoldStatus.FOLD_STATUS_EXPANDED 展开// FoldStatus.FOLD_STATUS_FOLDED 折叠// FoldStatus.FOLL_STATUS_HALF_FOLDED 半折叠getFoldStatusSync()只在折叠屏设备上返回有效值,普通设备返回UNKNOWN。
监听折叠状态变化:
display.on('foldStatusChange',(status:display.FoldStatus)=>{if(status===display.FoldStatus.FOLD_STATUS_EXPANDED){// 展开态 → 切Split}elseif(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满足不了需求时才考虑。