一、多设备布局到底是个啥

做鸿蒙开发最头疼的是什么?不是代码难写,是设备太多。手机、折叠屏、平板、PC,每种设备屏幕尺寸都不一样。你写一套代码,想在这些设备上都能跑,界面还得好看,这就得多设备适配。

华为搞了个"一次开发,多端部署"的理念,说白了就是让你写一套代码,自动适配所有设备。咋实现的?靠的就是响应式布局。

响应式布局的核心是两个东西:断点栅格

断点是啥?就是把屏幕宽度分成几个区间,不同区间用不同的布局策略。比如手机屏幕小,就用 sm 断点;平板屏幕大,就用 lg 断点。

栅格是啥?就是把屏幕划分成若干等分的格子,组件占多少格子,你说了算。比如手机屏幕分成 4 格,平板分成 12 格,同一个组件在不同设备上占不同的格子数,布局就自适应了。

上图展示了四种响应式布局方式对应的不同场景和实现方案。重复布局适合列表、瀑布流这些;分栏布局适合侧边栏、导航这些;挪移布局适合图文组合、导航切换;缩进布局适合居中展示。
在这里插入图片描述


二、断点机制先搞懂

断点是把屏幕宽度划分成几个区间,每个区间对应一种设备形态。HarmonyOS 定义了四个横向断点:

断点 屏幕宽度 对应设备
sm < 600vp 手机
md 600vp - 840vp 折叠屏、小平板
lg 840vp - 1440vp 平板
xl > 1440vp PC、2in1 设备

vp 是虚拟像素,HarmonyOS 用来做布局的单位。为啥用 vp 不用 px?因为不同设备像素密度不一样,用 vp 能保证在不同设备上显示效果一致。

断点咋用?你得先拿到当前设备的断点值,然后根据断点值动态调整组件属性。官方给了一个 WidthBreakpointType 类,专门用来根据断点返回不同的值:

new WidthBreakpointType(1, 2, 3, 3).getValue(this.mainWindowInfo.widthBp)

上面这行代码啥意思?sm 断点返回 1,md 断点返回 2,lg 断点返回 3,xl 断点也返回 3。你可以用这个机制动态控制组件属性。


三、重复布局:空间够了就多展示

重复布局是最常用的方式。啥意思?屏幕小的时候展示少一点,屏幕大了就多展示一些,用相同或相似的结构重复排列。

重复布局包含四种:列表布局、瀑布流布局、轮播布局、网格布局。

1. 列表布局

List 组件有个 lanes 属性,可以控制列数。结合断点,就能实现不同设备展示不同列数:

List({
  space: new WidthBreakpointType(8, 12, 16, 16).getValue(this.mainWindowInfo.widthBp),
  scroller: this.listScroller
}) {
  // ...
}
.scrollBar(BarState.Off)
.lanes(new WidthBreakpointType(1, 2, 3, 3).getValue(this.mainWindowInfo.widthBp), 12)

这段代码有几个关键点:

  • space 控制行间距,sm 断点用 8vp,md 用 12vp,lg 和 xl 用 16vp
  • lanes 控制列数,sm 单列,md 双列,lg 和 xl 三列
  • 第二个参数 12 是列间距

不同断点下的布局效果:

从上图能看到,sm 断点是单列列表,行间距 8vp。空间不够,就展示最少的列数。

md 断点是双列列表,列间距 12vp,行间距 12vp。折叠屏上,空间够展示两列。

lg 断点是三列列表,列间距 12vp,行间距 16vp。平板上,空间够展示三列。
在这里插入图片描述

2. 瀑布流布局

瀑布流跟列表类似,区别是瀑布流每个元素高度不一样。WaterFlow 组件专门干这事儿:

WaterFlow() {
  LazyForEach(this.dataSource, (item: number, index: number) => {
    FlowItem() {
      Row() {}
      .width('100%')
      .height('100%')
      .borderRadius(16)
      .backgroundColor('#F1F3F5')
    }
    .width('100%')
    .height(this.itemHeightArray[index])
  }, (item: number, index: number) => JSON.stringify(item) + index)
}
.columnsTemplate(`repeat(${new WidthBreakpointType(2, 3, 4, 4).getValue(this.mainWindowInfo.widthBp)}, 1fr)`)
.columnsGap(12)
.rowsGap(12)
.width('100%')

关键属性是 columnsTemplate,用 repeat() 函数动态控制列数。sm 双列,md 三列,lg 和 xl 四列。

sm 断点是双列瀑布流,每个元素高度不一,错落有致。

md 断点是三列瀑布流,列间距 12vp,行间距 12vp。

lg 断点是四列瀑布流,充分利用平板的大屏空间。
在这里插入图片描述

瀑布流适合展示高度不一的内容,比如商品卡片、新闻卡片、社交媒体信息流。

3. 轮播布局

轮播也能做多设备适配。Swiper 组件有个 displayCount 属性,控制视窗内展示几个元素:

Swiper() {
  // ...
}
.displayCount(new WidthBreakpointType(1, 2, 3, 3).getValue(this.mainWindowInfo.widthBp))
.indicator(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? Indicator.dot()
  .itemWidth(6)
  .itemHeight(6)
  .selectedItemWidth(12)
  .selectedItemHeight(6)
  .color('#4DFFFFFF')
  .selectedColor(Color.White) : false
)
.prevMargin(new WidthBreakpointType(0, 12, 64, 64).getValue(this.mainWindowInfo.widthBp))
.nextMargin(new WidthBreakpointType(0, 12, 64, 64).getValue(this.mainWindowInfo.widthBp))

这段代码有几个细节:

  • displayCount 控制视窗内元素个数,手机一个,折叠屏两个,平板三个
  • indicator 只在手机上显示圆点指示器,其他设备不显示
  • prevMarginnextMargin 控制前后边距,手机没边距,折叠屏 12vp,平板 64vp

sm 断点展示一个元素,无前后边距,显示圆点指示器。

md 断点展示两个元素,前后边距 12vp,不显示指示器。

lg 断点展示三个元素,前后边距 64vp,能看到前后各露出一点相邻卡片。
在这里插入图片描述

轮播布局适合展示推荐内容、广告轮播、精选商品这些场景。

4. 网格布局

Grid 组件跟 WaterFlow 类似,区别是 Grid 的子组件高度一致,严格对齐。适合展示规则的内容:

Grid() {
  ForEach(this.infoArray.slice(new WidthBreakpointType(4, 2, 0, 0).getValue(this.mainWindowInfo.widthBp)),
    (item: number) => {
      // ...
    }, (item: number, index: number) => JSON.stringify(item) + index)
}
.width('100%')
.columnsTemplate(`repeat(${new WidthBreakpointType(2, 3, 4, 4).getValue(this.mainWindowInfo.widthBp)}, 1fr)`)
.columnsGap(12)
.rowsGap(12)

Grid 还有个 rowsTemplate 属性可以控制行数。不设置的话,行数 = 展示元素数量 / 列数。

sm 断点是 2 行 2 列网格,每个元素高度一致,严格对齐。

md 断点是 2 行 3 列网格,列间距 12vp,行间距 12vp。

lg 断点是 2 行 4 列网格,充分利用平板的大屏空间展示更多内容。
在这里插入图片描述

网格布局适合展示功能入口、快捷操作、应用图标这些场景。注意网格和瀑布流的区别:网格元素高度一致、严格对齐;瀑布流元素高度不一、错落有致。


四、分栏布局:空间够了就分屏展示

分栏布局是把窗口划分成两栏或三栏,每栏展示不同内容。这种方式在大屏设备上特别有用,能充分利用空间。

分栏布局包含三种:侧边栏、单/双栏、三分栏。

1. 侧边栏

SideBarContainer 组件能实现侧边栏效果:

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%', '40%').getValue(this.mainWindowInfo.widthBp))
.controlButton({ top: this.getUIContext().px2vp(this.mainWindowInfo.AvoidSystem?.topRect.height) + 12 })

这段代码有几个关键点:

  • SideBarContainerType 控制侧边栏类型,sm 断点用 Overlay(浮层),其他断点用 Embed(嵌入)
  • showSideBar 控制是否显示侧边栏
  • sideBarWidth 控制侧边栏宽度,sm 80%,md 50%,lg 40%

sm 断点默认不显示侧边栏,侧边栏浮在内容区上,宽度 80%。点击控制按钮才会显示。


在这里插入图片描述

md 断点默认显示侧边栏,侧边栏和内容区并列展示,宽度 50%。

<img src="https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/6b/v3/INfyvnWsQ-uWjtO0CVUZ9w/zh-cn_image_0000002509247141.png?HW-CC-KV=V1&HW-CC-Date=20260413T014300Z&HW-CC-Expire=86400&HW-CC-Sign=71053022E7CB713C9964D29F90FD1CAA12B3C5FD70F9FCBFDF288D42DB8EEC20" title="null" crop="0,0,1,1" id="c46tl" class="ne-image">

lg 断点默认显示侧边栏,侧边栏和内容区并列展示,宽度 40%。平板上侧边栏占的比例更小,内容区更大。
在这里插入图片描述

2. 单/双栏

Navigation 组件能实现单/双栏效果:

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

关键属性是 mode,sm 断点用 Stack(单栏),其他断点用 Split(双栏)。

sm 断点是单栏显示,导航栏和内容区单栏展示,点击导航栏条目跳转到内容区。


在这里插入图片描述

md 断点是双栏显示,导航栏和内容区分栏展示,导航栏宽度 50%。

lg 断点是双栏显示,导航栏和内容区分栏展示,导航栏宽度 50%。
在这里插入图片描述

单/双栏和侧边栏的区别是:单/双栏的导航栏能控制内容区路由跳转,比如商品列表和商品详情;侧边栏通常不控制内容区内容,比如图文详情和评论区。

3. 聊天场景的特殊处理

聊天场景有个特殊需求:双栏布局下,点击商品链接要全屏展示商品页,隐藏原聊天页。咋实现?

@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
}

关键逻辑在 getNavMode() 方法:默认双栏模式(Split),点击商品链接后,设置 isNavFullScreen = true,切换到单栏模式(Stack);返回时,设置 isNavFullScreen = false,恢复双栏模式。

sm 断点是单栏显示,导航栏和内容区单栏展示。


在这里插入图片描述
在这里插入图片描述

md 断点是双栏显示,导航栏和内容区分栏展示,内容扩展区单独展示,导航栏宽度 50%。

在这里插入图片描述

lg 和 xl 断点是双栏显示,导航栏和内容区分栏展示,内容扩展区单独展示,导航栏宽度 50%。
在这里插入图片描述
在这里插入图片描述

4. 三分栏

三分栏需要结合 SideBarContainer 和 Navigation:

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))

sm 断点默认不显示侧边栏,侧边栏覆盖导航栏,宽度 80%,导航栏和内容区单栏显示。


在这里插入图片描述

在这里插入图片描述

md 断点默认不显示侧边栏,侧边栏覆盖导航栏,宽度 50%,导航栏和内容区分栏展示,导航栏宽度 50%。


在这里插入图片描述

lg 断点默认显示侧边栏,侧边栏嵌入导航栏,宽度 20%,导航栏和内容区分栏展示,导航栏宽度 30%。
在这里插入图片描述

三分栏适合邮箱、日历这些有三层结构的应用。

5. 邮箱场景

邮箱有三个层级:账户信息 -> 收件箱 -> 邮件详情。三分栏正好匹配:

上图展示了不同断点下的默认效果,lg 和 xl 断点默认显示侧边栏,其他断点默认不显示。

上图展示了不同断点下的内容效果,选中邮件后展示邮件详情。

实现代码关键是控制 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)) {
        // 区域 A:账户信息
        Column() {
          MailSideBar()
        }
        .width('100%')
        .height('100%')
        .backgroundColor($r('sys.color.gray_01'))

        // 区域 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)
    }
  }
}

6. 日历场景的特殊处理

日历有个特殊情况:单栏布局下,应该优先展示日历(导航栏),而不是日程(内容区)。

上图展示了日历在不同断点下的布局效果,单栏优先展示日历,双栏展示日历和日程,三栏展示账户信息、日历和日程。

实现关键是利用 onNavigationModeChange 回调,单栏时清空路由栈,只显示导航栏(日历):

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. 图文组合布局

比如音乐播放页,手机上是封面在上、歌曲列表在下;平板上是封面在左、歌曲列表在右。

GridRow 和 GridCol 组件能实现这种效果:

GridRow({
  columns: { xs: 4, sm: 4, md: 8, lg: 12, xl: 12 },
  gutter: 0,
  breakpoints: { value: ['320vp', '600vp', '840vp', '1440vp']},
  direction: GridRowDirection.Row
}) {
  GridCol({
    span: { xs: 4, sm: 4, md: 4, lg: 4, xl: 4 },
    offset: 0
  }) {
    // 封面区
  }
  .height(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? this.getGridColHeight() : '100%')
  .padding({ top: this.getUIContext().px2vp(this.mainWindowInfo.AvoidSystem?.topRect.height) + 12})
  .backgroundColor('#AAD3F1')

  GridCol({
    span: { xs: 4, sm: 4, md: 4, lg: 8, xl: 8 },
    offset: 0
  }) {
    // 歌曲列表区
  }
  .backgroundColor(Color.Pink)
  .layoutWeight(1)
  .padding({ top: this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_SM ? 0 :
    this.getUIContext().px2vp(this.mainWindowInfo.AvoidSystem?.topRect.height) })
}

关键属性是 span,控制组件占多少栅格:

  • 手机:封面 4 格(占满),歌曲列表 4 格(占满)—— 上下布局
  • 平板:封面 4 格,歌曲列表 8 格 —— 左右布局

sm 断点整个窗口划分为 4 栅格,歌单封面区(蓝)占 4 栅格,歌曲列表区(粉)占 4 栅格,上下布局。

md 断点整个窗口划分为 8 栅格,歌单封面区(蓝)占 4 栅格,歌曲列表区(粉)占 4 栅格,左右布局。

lg 断点整个窗口划分为 12 栅格,歌单封面区(蓝)占 4 栅格,歌曲列表区(粉)占 8 栅格,左右布局,歌曲列表占更大比例。

这种布局也适合页面顶部页签与搜索框的组合,页签在上、搜索框在下变成页签在左、搜索框在右。

上图展示了页签和搜索框在不同断点下的布局效果,从上下布局变成左右布局。

2. 底部/侧边导航

Tabs 组件能实现底部导航和侧边导航的切换:

Tabs({
  barPosition: this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_LG ? BarPosition.Start : BarPosition.End
}) {
  TabContent() {
    TopTabView({
      pageInfos: this.pageInfos,
      mainWindowInfo: this.mainWindowInfo,
      firstLevelIndex: this.firstLevelIndex,
      tabData: this.tabData
    })
  }
  .tabBar(this.tabBuilder(this.firstTabList[0], 0))

  TabContent()
    .tabBar(this.tabBuilder(this.firstTabList[1], 1))

  TabContent()
    .tabBar(this.tabBuilder(this.firstTabList[2], 2))

  TabContent()
    .tabBar(this.tabBuilder(this.firstTabList[3], 3))
}
.barBackgroundColor('#CCF1F3F5')
.barWidth(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_LG ? 96 : '100%')
.barHeight(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_LG ? '100%' : 56 + this.getUIContext().px2vp(this.mainWindowInfo.AvoidNavigationIndicator?.bottomRect.height))
.barMode(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_LG ? BarMode.Scrollable : BarMode.Fixed,
  { nonScrollableLayoutStyle: LayoutStyle.ALWAYS_CENTER })
.barBackgroundBlurStyle(BlurStyle.COMPONENT_THICK)
.vertical(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_LG)
.onChange((index: number) => {
  this.firstLevelIndex = index;
})

关键属性:

  • barPosition:sm 和 md 用 End(底部),lg 用 Start(侧边)
  • vertical:lg 断点设为 true,导航栏就变成垂直方向
  • barWidthbarHeight:控制导航栏尺寸

sm 断点分级导航由底部一级导航栏和顶部二级页签组成。

md 断点分级导航由底部一级导航栏和顶部二级页签组成,和 sm 断点类似。

lg 断点分级导航由侧边一级导航栏和顶部二级页签组成,一级导航栏变成侧边。

xl 断点或 PC/2in1 设备,通过侧边栏显示一级和二级导航,导航栏在左侧。

xl 断点或 PC 设备上,可以用 SideBarContainer 实现一级和二级导航的侧边栏:

if ((this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_LG || this.mainWindowInfo.widthBp
  === WidthBreakpoint.WIDTH_XL)&& deviceInfo.deviceType == "2in1") {
  SideBarContainer(SideBarContainerType.Embed) {
    TabSideBarView({
      firstLevelIndex: this.firstLevelIndex,
      secondLevelIndex: this.secondLevelIndex,
      tabData: this.tabData,
      firstTabList: this.firstTabList
    })
    Column() {
      Row() {
        Text(this.tabViewModel.getTabNameOfSecondLevel(this.tabViewModel.getTabNameOfFirstLevel(this.firstLevelIndex),
          this.secondLevelIndex))
          .fontSize('20fp')
          .fontWeight(700)
          .margin({ left: 16 })
      }
      .padding({ top: 60, bottom: 14 })

      VideoInfoView({
        mainWindowInfo: this.mainWindowInfo,
        firstLevelIndex: this.firstLevelIndex,
        secondLevelIndex: this.secondLevelIndex
      })
    }
    .alignItems(HorizontalAlign.Start)
  }
  .autoHide(false)
  .divider({ strokeWidth: 0.3 })
  .showControlButton(false)
  .sideBarWidth(240)
  .minSideBarWidth(240)
  .maxSideBarWidth(240)
} else {
  // ...
}

六、缩进布局:居中展示留白更优雅

缩进布局是把内容居中展示,两侧留白。这种方式适合单列列表、表单这些场景。

GridRow 和 GridCol 能实现缩进布局,关键是用 offset 属性控制偏移:

GridRow({
  columns: { xs: 4, sm: 4, md: 8, lg: 12, xl: 12 },
  gutter: 0,
  breakpoints: { value: ['320vp', '600vp', '840vp', '1440vp']},
  direction: GridRowDirection.Row
}) {
  GridCol({
    span: { xs: 4, sm: 4, md: 6, lg: 8, xl: 8 },
    offset: { xs: 0, sm: 0, md: 1, lg: 2, xl: 2 }
  }) {
    // 内容
  }
  .width('100%')
  .height('100%')
}

关键参数:

  • span:内容占多少栅格
  • offset:内容偏移多少栅格

效果:

  • 手机:占 4 格,偏移 0 —— 占满屏幕
  • 平板:占 6 格,偏移 1 —— 两侧各留 1 格
  • PC:占 8 格,偏移 2 —— 两侧各留 2 格
    在这里插入图片描述

sm 断点整个窗口划分为 4 栅格,单列列表占 4 列,偏移 0 列,占满屏幕。

md 断点整个窗口划分为 8 栅格,单列列表占 6 列,两侧各偏移 1 列,居中展示。

lg 断点整个窗口划分为 12 栅格,单列列表占 8 列,两侧各偏移 2 列,居中展示,留白更多。

缩进布局适合登录表单、设置页面、详情页这些场景,居中展示让内容更聚焦,两侧留白让视觉更舒适。


七、关键组件和属性总结

整理一下几种布局用到的核心组件和属性:

布局方式 核心组件 关键属性
列表布局 List lanes、space
瀑布流布局 WaterFlow columnsTemplate、columnsGap、rowsGap
轮播布局 Swiper displayCount、prevMargin、nextMargin、indicator
网格布局 Grid columnsTemplate、rowsTemplate、columnsGap、rowsGap
侧边栏 SideBarContainer showSideBar、sideBarWidth、SideBarContainerType
单/双栏 Navigation mode、navBarWidth、onNavigationModeChange
三分栏 SideBarContainer + Navigation 组合使用
图文组合 GridRow + GridCol span、offset、columns
导航切换 Tabs barPosition、vertical、barWidth、barHeight
缩进布局 GridRow + GridCol span、offset

WidthBreakpointType 类是断点适配的关键工具,它能根据当前断点返回不同的值。使用方式:

new WidthBreakpointType(sm值, md值, lg值, xl值).getValue(this.mainWindowInfo.widthBp)

八、踩坑记录

坑 1:断点值获取时机

我一开始在组件构造时获取断点值,结果发现断点值不对。后来才知道,断点值要在窗口尺寸变化时更新。

正确做法是订阅窗口尺寸变化事件,在回调里更新断点值:

window.on('windowSizeChange', (size) => {
  this.mainWindowInfo.widthBp = this.getWidthBreakpoint(size.width);
});

坑 2:侧边栏宽度百分比计算

SideBarContainer 的 sideBarWidth 属性支持百分比,但百分比是相对于父容器宽度计算的。如果父容器没设置宽度,百分比就会出问题。

建议给 SideBarContainer 的父容器设置明确宽度,或者用 width('100%')

坑 3:Navigation mode 切换时机

Navigation 的 mode 属性切换时,会触发 onNavigationModeChange 回调。但回调触发时机有个坑:首次渲染时会触发一次。

如果你在回调里做了路由操作,要注意首次渲染时别误操作。可以加个判断:

.onNavigationModeChange((mode: NavigationMode) => {
  if (this.isFirstRender) {
    this.isFirstRender = false;
    return; // 首次渲染不处理
  }
  // 正常处理
})

坑 4:GridCol 的 layoutWeight

GridCol 组件默认会用 layoutWeight 来撑满剩余空间,但有时候会和 span 属性冲突。

如果你想用 span 精确控制栅格数,就不要用 layoutWeight。反之,如果用 layoutWeight,就别设 span

坑 5:Tabs 的 barHeight 在手机上要加导航指示器高度

手机上有导航指示器,Tabs 的 barHeight 要加上导航指示器高度,不然导航栏会被遮挡:

.barHeight(this.mainWindowInfo.widthBp === WidthBreakpoint.WIDTH_LG ? '100%' : 56 + this.getUIContext().px2vp(this.mainWindowInfo.AvoidNavigationIndicator?.bottomRect.height))

坑 6:瀑布流和网格的选择

瀑布流和网格看起来差不多,但适用场景不一样:

  • 瀑布流:子组件高度不一,适合商品卡片、新闻卡片这些高度不一致的内容
  • 网格:子组件高度一致,适合功能入口、应用图标这些高度一致的内容

选错了组件,布局效果会很别扭。


九、总结

多设备页面布局的核心是响应式,响应式的核心是断点和栅格。

四种布局方式覆盖了大部分场景:

  • 重复布局:空间够了就多展示,适合列表、瀑布流、轮播、网格
  • 分栏布局:空间够了就分屏,适合侧边栏、导航、邮箱、日历
  • 挪移布局:位置挪挪更舒适,适合图文组合、导航切换
  • 缩进布局:居中展示留白更优雅,适合表单、详情页

掌握了这四种方式,加上断点机制和栅格系统,多设备适配就不是啥难事了。

关键是搞清楚每种场景适合哪种布局方式,然后选对应的组件,设置对应的属性。别硬套公式,得根据实际场景灵活调整。比如聊天场景要全屏展示商品页,日历场景要优先展示日历,这些特殊需求要用特殊方法处理。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐