【高心星出品】

鸿蒙PC开发——分栏布局

分栏布局是指在空间充足时,将窗口划分为两栏或三栏,用于展示多类内容。常见的分栏布局包括侧边栏、单/双栏和三分栏。

侧边栏 SideBarContainer组件+断点
侧边分级导航栏 SideBarContainer组件+断点
三分栏 SideBarContainer组件+Navigation组件+断点

媒体查询工具类

export class WidthBreakpointType<T> {
  sm: T;
  md: T;
  lg: T;
  // 构造函数
  constructor(sm: T, md: T, lg: T) {
    this.sm = sm;
    this.md = md;
    this.lg = lg;
  }
  // 根据断点返回值
  getValue(widthBp: WidthBreakpoint): T {
    if (widthBp === WidthBreakpoint.WIDTH_XS || widthBp === WidthBreakpoint.WIDTH_SM) {
      return this.sm;
    }
    if (widthBp === WidthBreakpoint.WIDTH_MD) {
      return this.md;
    } else {
      return this.lg;
    }
  }
}

侧边栏

侧边栏基于横向断点,动态控制侧边栏是否显示,实现二分栏布局。

布局效果

实现方案

在不同横向断点下,动态控制SideBarContainer组件的showSideBar和sideBarWidth属性实现目标效果。

示例代码

SideBarContainer(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? SideBarContainerType.Overlay :
  SideBarContainerType.Embed) {
  Column() {
    // ...
  }
  .backgroundColor('#F1F3F5')

  Column() {
    // ...
  }
  .backgroundColor('#FDBFFC')
  .padding({
    top: this.getUIContext().px2vp(this.mainWindowInfo.AvoidSystem?.topRect.height) + 12,
    bottom: this.getUIContext().px2vp(this.mainWindowInfo.AvoidNavigationIndicator?.bottomRect.height),
    left: 16,
    right: 16
  })
}
.showSideBar(this.isShowingSidebar)
.sideBarWidth(new WidthBreakpointType('80%', '50%', '40%').getValue(this.mainWindowInfo.widthBp))
.controlButton({ top: this.getUIContext().px2vp(this.mainWindowInfo.AvoidSystem?.topRect.height) + 12 })

代码逻辑走读:

  1. 侧边栏容器类型选择
    • 使用三元运算符根据this.mainWindowInfo.widthBp的值选择侧边栏容器类型。如果宽度断点为WIDTH_SM,则选择SideBarContainerType.Overlay,否则选择SideBarContainerType.Embed
  2. 侧边栏内容定义
    • 定义了两个Column组件,分别设置了不同的背景颜色。第一个Column没有设置背景颜色,第二个Column的背景颜色为#FDBFFC
  3. 侧边栏内边距设置
    • 第二个Column设置了内边距,顶部内边距为系统状态栏高度加上12,底部内边距为导航指示器高度,左右内边距为16。
  4. 侧边栏显示控制
    • 使用.showSideBar(this.isShowingSidebar)方法控制侧边栏的显示状态。
  5. 侧边栏宽度设置
    • 使用.sideBarWidth(new WidthBreakpointType('80%', '50%', '40%').getValue(this.mainWindowInfo.widthBp))方法根据窗口的宽度断点动态设置侧边栏的宽度。
  6. 控制按钮位置设置
    • 使用.controlButton({ top: this.getUIContext().px2vp(this.mainWindowInfo.AvoidSystem?.topRect.height) + 12 })方法设置控制按钮的顶部位置,位置等于系统状态栏高度加上12。

单/双栏

单/双栏基于横向断点,动态控制导航栏的显示模式,实现二分栏布局。

布局效果

在这里插入图片描述

实现方案

设置不同横向断点下,Navigation组件的mode属性实现目标效果。

示例代码

Navigation(this.pathStack) {
  // ...
}
.mode(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? NavigationMode.Stack : NavigationMode.Split)

代码逻辑走读:

  1. 导航初始化
    • 使用 Navigation(this.pathStack)初始化导航组件,this.pathStack可能是一个用于存储导航路径的栈结构。
  2. 注释部分
    • 注释 // ...表示代码片段中可能存在其他逻辑或配置,但这些部分在此代码片段中未被展示。
  3. 模式选择
    • 使用 .mode()方法设置导航模式。
    • 判断条件 this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM判断当前窗口的宽度是否属于小尺寸(WIDTH_SM)。
    • 根据判断结果,选择不同的导航模式:
      • 如果宽度是小尺寸,选择 NavigationMode.Stack(堆栈模式)。
      • 否则,选择 NavigationMode.Split(分屏模式)。

单/双栏与侧边栏的主要区别是单/双栏的导航栏能够控制内容区的路由跳转,例如商品列表与商品详情;侧边栏通常不控制内容区展示的内容,例如图文详情与评论区。

二分栏典型场景——聊天

某些应用在双栏布局下支持通过右侧内容区链接跳转至其扩展页面并单栏展示。以社交应用为例,在横向断点为md、lg和xl时,左侧导航栏为聊天列表,右侧内容区显示聊天框,包括文字信息和商品链接;当用户在右侧点击商品链接时,可进入单栏模式,全屏展示对应的商品扩展区页面,同时隐藏原聊天页,实现沉浸式浏览体验。

布局效果

在这里插入图片描述
实现方案

使用Navigation组件,动态控制其NavigationMode,在路由时切换单双栏显示。聊天列表作为Navigation导航栏,通过点击不同列表条目实现右侧Navigation内容区的路由控制,这种分栏路由模式为NavigationMode.Split。点击商品链接时,设置全屏变量isNavFullScreen为true,切换Navigation为单栏展示,此时路由模式为NavigationMode.Stack。路由返回时,isNavFullScreen变量改为false,路由重新切换为NavigationMode.Split。

对于聊天页面的双栏路由模式切换,开发者可以抽象为:

在这里插入图片描述

示例代码

@Builder
PageMap(name: string) {
  if (name === 'conversationDetail') {
    ConversationDetail({
      // ...
    })
  } else if (name === 'conversationDetailNone') {
    ConversationDetailNone();
  } else if (name === 'productPage') {
    ProductPage({
      // ...
    })
  }
}

build() {
  Navigation(this.pathStack) {
    ConversationNavBarView({
      mainWindowInfo: this.mainWindowInfo,
      pageInfos: this.pageInfos,
      pathStack: this.pathStack,
    })
  }
  .mode(this.getNavMode())
  // ...
  .navDestination(this.PageMap)
}

getNavMode(): NavigationMode {
  if (!this.isNavFullScreen && this.mainWindowInfo.widthBp !== WidthBreakpoint.WIDTH_SM) {
    return NavigationMode.Split;
  }
  return NavigationMode.Stack
}

代码逻辑走读:

  1. PageMap构建器:定义了一个名为PageMap的构建器函数,接受一个name参数。根据name的值,渲染不同的页面组件。
    • 如果name'conversationDetail',则渲染ConversationDetail组件。
    • 如果name'conversationDetailNone',则渲染ConversationDetailNone组件。
    • 如果name'productPage',则渲染ProductPage组件。
  2. build方法:定义了一个build方法,用于构建导航组件。
    • 使用Navigation组件,传入this.pathStack作为参数。
    • Navigation组件内部,渲染ConversationNavBarView组件,传入多个属性参数。
    • 调用mode方法,传入this.getNavMode()的返回值,设置导航模式。
    • 调用navDestination方法,传入this.PageMap,设置导航的目标。
  3. getNavMode方法:定义了一个getNavMode方法,用于根据条件返回导航模式。
    • 如果this.isNavFullScreenfalsethis.mainWindowInfo.widthBp不等于WidthBreakpoint.WIDTH_SM,则返回NavigationMode.Split
    • 否则,返回NavigationMode.Stack

三分栏

三分栏基于横向断点,动态控制导航栏的显示模式和侧边栏是否显示,实现三分栏布局。

布局效果

横向断点 sm md lg
属性 默认不显示侧边栏侧边栏覆盖导航栏侧边栏宽度80%导航栏和内容区单栏显示 默认不显示侧边栏侧边栏覆盖导航栏侧边栏宽度50%导航栏和内容区分栏显示导航栏宽度50% 默认显示侧边栏侧边栏嵌入导航栏侧边栏宽度20%导航栏和内容区分栏显示导航栏宽度30%
展示布局 在这里插入图片描述在这里插入图片描述在这里插入图片描述 在这里插入图片描述 请添加图片描述

实现方案

在不同横向断点下,动态控制SideBarContainer组件的showSideBar、sideBarWidth属性,和Navigation组件的mode、navBarWidth属性实现目标效果。

示例代码

SideBarContainer(new WidthBreakpointType(SideBarContainerType.Overlay, SideBarContainerType.Overlay,
  SideBarContainerType.Embed, SideBarContainerType.Embed).getValue(this.mainWindowInfo.widthBp)) {
  Column() {
    // ...
  }
  // ...

  Column() {
    Navigation(this.pathStack) {
      NavigationBarView({
        mainWindowInfo: this.mainWindowInfo,
        pageInfos: this.pageInfos,
        pathStack: this.pathStack,
        isShowingSidebar: this.isShowingSidebar,
        isTriView: true
      })
    }
    .width('100%')
    .height('100%')
    .mode(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? NavigationMode.Stack : NavigationMode.Split)
    .navBarWidth(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_MD ? '50%' : '40%')
    .navDestination(this.PageMap)
    .backgroundColor('#B8EEB2')
  }
  // ...
}
.showSideBar(this.isShowingSidebar)
.sideBarWidth(new WidthBreakpointType('80%', '50%', '20%', '20%').getValue(this.mainWindowInfo.widthBp))

代码逻辑走读:

  1. 创建侧边栏容器:使用SideBarContainer组件,并通过new WidthBreakpointType对象动态设置侧边栏的显示状态和宽度。
  2. 定义侧边栏内容:在SideBarContainer内部使用Column组件定义侧边栏的内容,这里包含了两个Column组件,分别用于不同布局的页面内容。
  3. 导航栏配置:在第二个Column组件中,使用Navigation组件配置导航栏,通过NavigationBarView组件定义导航栏的内容和行为。
  4. 导航栏样式和行为:设置导航栏的宽度、模式、导航目标、背景颜色等属性,根据窗口的宽度断点调整导航栏的宽度和模式。
  5. 侧边栏显示控制:使用showSideBar方法控制侧边栏的显示状态,侧边栏的宽度也根据窗口的宽度断点动态调整。

三分栏典型场景——邮箱

在邮箱场景中,单栏状态下,默认展示收件箱页,当选择邮件对象后,展示邮件详情页。双栏和三栏状态下,右侧默认不展示邮件详情页,当选择邮件对象后,右侧展示邮件详情页。

布局效果

在这里插入图片描述

邮箱分为三个层级目录:第一层为账户信息,第二层为收件箱(一个账户信息对应多条邮件信息),第三层为邮件详情。根据内容的重要性,开发者在单栏模式下显示邮件详情,在双栏模式下显示收件箱和邮件详情,在三栏模式下显示账户信息、收件箱和邮件详情。

对于邮箱页面的一多分栏变化,开发者可以抽象为:

在这里插入图片描述

示例代码

  • 对SideBarContainer组件的showSideBar属性进行赋值,如果横向断点为lg或xl,则默认显示侧边栏,反之,则默认不显示。

    build() {
      GridRow() {
        GridCol({ span: { sm: 12, md: 12, lg: 12, xl: 12 } }) {
          SideBarContainer(new WidthBreakpointType(SideBarContainerType.Overlay, SideBarContainerType.Overlay,
            SideBarContainerType.Embed, SideBarContainerType.Embed).getValue(this.mainWindowInfo.widthBp)) {
            // Area A
            Column() {
              MailSideBar()
            }
            .width('100%')
            .height('100%')
            .backgroundColor($r('sys.color.gray_01'))
    
            // Area B+C
            Column() {
              Stack() {
                MailNavigation({
                  mainWindowInfo: this.mainWindowInfo,
                  pageInfos: this.pageInfos,
                  pathStack: this.pathStack,
                })
                  .margin({ top: 18 })
                  .padding({ left: this.getUIContext().px2vp(this.mainWindowInfo.AvoidSystem?.topRect.left) })
                // ...
              }
            }
            .width('100%')
            .height('100%')
          }
          .showSideBar(this.isShowingSidebar)
          // ...
        }
      }
      .width('100%')
      .height('100%')
    }
    

    代码逻辑走读:

    1. 构建网格行:使用 GridRow()创建一个网格行,用于在不同屏幕尺寸下布局。
    2. 定义网格列:使用 GridCol()创建一个网格列,设置跨度为全屏(sm: 12, md: 12, lg: 12, xl: 12)。
    3. 侧边栏容器:使用 SideBarContainer()创建一个侧边栏容器,容器类型根据窗口宽度断点动态调整。
    4. 区域A:在侧边栏容器中定义一个列(Column()),用于放置侧边栏内容,如 MailSideBar()
    5. 区域B+C:在另一个列中嵌套一个 Stack(),用于放置导航栏(MailNavigation())和其他可能的内容。
    6. 显示侧边栏:使用 .showSideBar(this.isShowingSidebar)控制侧边栏的显示状态。
    7. 设置宽度和高度:为所有容器和列设置宽度和高度为全屏(100%)。
  • 在SideBarContainer组件内容区中使用Navigation组件,对Navigation组件的mode属性进行赋值,如果断点为sm或xs,则为单栏,反之则为双栏。

    build() {
      Navigation(this.pathStack) {
        // ...
      }
      .mode(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? NavigationMode.Stack : this.notesNavMode)
      .navDestination(this.myRouter)
      // ...
    }
    

    代码逻辑走读:

    1. Navigation(this.pathStack):这部分代码初始化了一个导航系统,使用this.pathStack作为导航的路径栈。
    2. .mode(…):设置导航模式,根据this.mainWindowInfo.widthBp的值来决定导航模式。如果宽度小于某个阈值(WidthBreakpoint.WIDTH_SM),则使用NavigationMode.Stack,否则使用this.notesNavMode
    3. .navDestination(this.myRouter):设置导航的目标或目的地,即使用this.myRouter作为导航的目标。

三分栏典型场景——日历

在三分栏的单栏布局中,通常展示的重点是Navigation的内容区。但在某些场景下,内容区的优先级低于导航区,例如日历日程功能。在这种情况下,单栏布局会优先展示日历(即Navigation的导航区)。效果如下:

在这里插入图片描述

日历日程分为三个层级,账户信息->日历->日程,开发者通常在单栏显示日历,双栏显示日历、日程,三栏显示账户信息、日历、日程。日历日程页面与邮箱页面的主要区别在于,日历日程页面的单栏页面重点显示Navigation导航栏,邮箱页面的单栏重点显示Navigation内容区。

布局效果

在这里插入图片描述

示例代码

在Navigation的onNavigationModeChange属性中进行判断,当Navigation首次显示或单双栏状态发生变化时。

  • 如果是单栏,则清空PathInfo路由,Navigation的内容区不显示,即可实现单屏显示Navigation导航栏的目的。

  • 如果为双栏,则重新向PathInfo路由中push内容区参数,即可实现Navigation内容区显示具体日程的目的。

    Row() {
      // ...
    
      if (this.mainWindowInfo.widthBp !== WidthBreakpoint.WIDTH_SM) {
        Column() {
          // ...
        }
        // ...
        .onClick(() => {
          if (this.navMode === NavigationMode.Split) {
            this.navMode = NavigationMode.Stack;
          } else if (this.navMode === NavigationMode.Stack && this.selectedItem.isTrip) {
            this.navMode = NavigationMode.Split;
          }
        })
      }
      // ...
    Navigation(this.pathStack) {
      CalendarView({
        mainWindowInfo: this.mainWindowInfo,
        pathStack: this.pathStack,
      })
    }
    .navDestination(this.pageMap)
    .mode(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? NavigationMode.Stack : this.navMode)
    // ...
    .onNavigationModeChange((mode: NavigationMode) => {
      if (this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM || mode === NavigationMode.Stack) {
        this.pathStack.clear();
      } else if (mode === NavigationMode.Split) {
        this.pathStack.pushPath({ name: this.selectedItem.date, param: this.selectedItem }, false);
      }
    })
    
    1. Row布局:代码首先定义了一个Row布局,这是一个容器组件,用于水平排列其子组件。
    2. 条件判断:在Row布局中,首先检查this.mainWindowInfo.widthBp是否不等于WidthBreakpoint.WIDTH_SM。如果条件为真,则执行后续的Column布局逻辑。
    3. Column布局:在满足条件的情况下,定义了一个Column布局,用于垂直排列其子组件。然而,代码中省略了具体的子组件内容。
    4. 点击事件处理:在Column布局中,为其添加了一个点击事件监听器。当点击事件触发时,会根据当前的this.navModethis.selectedItem.isTrip的值来更新this.navMode
    5. Navigation组件:代码中使用了一个Navigation组件,该组件用于管理导航路径栈。
    6. CalendarView组件:在Navigation组件内部,嵌套了一个CalendarView组件,该组件可能用于显示日历视图。
    7. 导航模式设置:通过.mode()方法,根据this.mainWindowInfo.widthBp的值来设置导航模式。如果宽度断点为WIDTH_SM,则使用NavigationMode.Stack模式,否则使用this.navMode
    8. 导航模式变化监听:通过.onNavigationModeChange()方法,监听导航模式的变化。当导航模式变化时,根据新的模式和当前的this.mainWindowInfo.widthBp的值来操作路径栈。
      Trip的值来更新this.navMode`。
    9. Navigation组件:代码中使用了一个Navigation组件,该组件用于管理导航路径栈。
    10. CalendarView组件:在Navigation组件内部,嵌套了一个CalendarView组件,该组件可能用于显示日历视图。
    11. 导航模式设置:通过.mode()方法,根据this.mainWindowInfo.widthBp的值来设置导航模式。如果宽度断点为WIDTH_SM,则使用NavigationMode.Stack模式,否则使用this.navMode
    12. 导航模式变化监听:通过.onNavigationModeChange()方法,监听导航模式的变化。当导航模式变化时,根据新的模式和当前的this.mainWindowInfo.widthBp的值来操作路径栈。
    13. 路径栈管理:在导航模式变化监听中,根据不同的条件来清空或添加路径栈中的路径。
Logo

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

更多推荐