HarmonyOS 6.0 分栏布局与折叠适配

分栏布局是平板和折叠屏应用的基本功。手机上用栈式导航,平板上用左右分栏,折叠屏要能在两种模式间无缝切换。HarmonyOS的Navigation组件内置了Split/Stack/Auto三种模式,但真要用好,还得理解底层的适配逻辑。

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满足不了需求时才考虑。

相关推荐
爱写代码的阿森1 小时前
鸿蒙三方库 | harmony-utils之PreferencesUtil首选项数据监听详解
服务器·华为·harmonyos·鸿蒙·huawei
爱写代码的森2 小时前
蒙三方库 | harmony-utils之FileUtil文件重命名与属性查询详解
linux·运维·服务器·华为·harmonyos·鸿蒙·huawei
二流小码农2 小时前
鸿蒙开发:以登录案例了解代码架构MVVM
android·ios·harmonyos
风华圆舞4 小时前
rawfile 资源与强类型词库加载器:schema / data / source 三层版本
harmonyos·arkui·resourcemanager·rawfile·arkts 编译·arkts 强类型
程序员黑豆5 小时前
鸿蒙应用开发:Grid组件实现九宫格布局教程
前端·华为·harmonyos
程序员黑豆6 小时前
鸿蒙应用开发:Flex 组件从入门到实战
前端·华为·harmonyos
<小智>6 小时前
鸿蒙多功能工具箱开发实战(二十)-性能优化与打包发布
ui·华为·harmonyos
不肥嘟嘟右卫门8 小时前
鸿蒙原生ArkTS布局方式之Popup+TextInput提示输入布局深度解析
华为·harmonyos
nullregedit9 小时前
原生鸿蒙像素画板实战 22:快捷键与鼠标交互
harmonyos·arkts·鸿蒙·bitart·像素画