大家好,我是那个刚从"人肉适配"噩梦(上一篇提到的)中走出来的老炮。上一篇我们把"一多"的理念想清楚了:分层架构、响应式布局、SysCap兼容。今天,我们钻进"术"的层面,拿我们最熟悉的战斗前线——界面布局开刀。
我知道很多朋友一听到"多端布局",脑子里本能地蹦出这样的代码:

if (windowWidth >= 600) {
  // 平板的双栏布局,一堆Row/Column和硬编码尺寸
} else if (windowWidth >= 840) {
  // 电脑的三栏布局,又是一堆复杂计算
} else {
  // 手机的单栏布局
}

一旦设计稿稍有变动,或者要新增一个设备类型,这些 if-else 就像藤蔓一样在代码里疯长,最后连自己都看不懂。今天我要做的就是 用鸿蒙ArkUI内置的"响应式组件",把这些"硬核"逻辑替换成"声明式"配置,实现真正的布局复用与优雅适配。

开篇:一次糟心的代码Review

我之前带过一个新人,他为了适配平板,在一个商品列表页里,写了将近100行的布局代码,里面塞满了基于 window.getWindowWidth() 计算的动态边距和 if 判断。代码的可读性和可维护性几乎为零。
我把他叫过来,指着屏幕说:“兄弟,咱们写代码不是做数学题。鸿蒙给了我们 Grid、GridRow/GridCol、Navigation、SideBarContainer 这些’高级积木’,你为啥还要用’小木块’和’胶水’(指硬编码和if-else)去硬拼呢?”
他挠挠头说:“老哥,这些组件我知道,但不知道它们能这么’聪明’啊!”
好,那今天我就用几个最常见的、也最容易写乱的场景,手把手给你演示,怎么用这些"聪明"的组件,把代码从"意大利面条"重构为"乐高模型"。

核心理念:响应式布局 vs 自适应布局

在动手前,我们再用一张图,巩固一下官方文档里这两个核心概念的区别,这决定了我们选用什么"武器"。

布局适配

响应式布局

自适应布局

整体结构大变样

内部元素微调

断点触发

容器尺寸变化

单栏变双栏

拉伸均分隐藏

Navigation/SideBarContainer

Flex/Grid/LayoutWeight

响应式布局:整体结构的质变。当窗口宽度跨越一个断点(如从 sm <600vp 到 md ≥600vp)时,页面布局方式发生根本改变。例如,从手机的单列列表,变为平板的左列表右详情。核心工具是:断点监听 和 响应式组件(如 Navigation、SideBarContainer)。
自适应布局:局部元素的量变。在相同的布局结构下,内部元素随容器尺寸变化进行拉伸、均分、缩放、隐藏等。核心工具是:七种自适应布局能力(我们稍后会细讲)。

一句话总结:响应式负责’翻篇’,自适应负责’润色’。 接下来,我们看四个实战场景。

实战一:从手机到电脑,Navigation如何优雅完成"单栏变双栏"?

这是最经典的场景。手机上是列表页,点击进入详情页。在平板上,我们希望直接左右分栏展示。
传统if-else做法: 你需要监听窗口宽度,动态创建两个 Column 容器,手动管理列表和详情的显示状态和路由逻辑。
"一多"优雅做法: 使用 Navigation 组件,只需改变一个属性——mode。


// 关键代码片段(基于声明式范式)
@Entry
@Component
struct ProductPage {
  // 假设这是从AppStorage或全局状态管理获取的当前窗口断点信息
  @StorageLink('currentWidthBreakpoint') widthBp: string = 'sm';

  build() {
    Navigation() {
      // 列表页作为NavBar
      ProductList()
    }
    .navDestination(this.productDetailBuilder) // 设置详情页构建器
    .mode(this.getNavMode()) // 核心:动态设置模式
  }

  // 根据断点决定导航模式
  private getNavMode(): NavigationMode {
    // 在sm(手机)下使用堆栈模式,其他情况使用分栏模式
    if (this.widthBp === 'sm') {
      return NavigationMode.Stack; // 单栏,全屏跳转
    } else {
      return NavigationMode.Split; // 双栏,并排显示
    }
  }

  @Builder
  productDetailBuilder() {
    ProductDetail()
  }
}

看到了吗?布局结构的根本性变化,被一个简单的 mode 属性配置所取代。 路由、动画、状态管理,Navigation 组件都帮你处理好了。你要做的,就是把 widthBp 这个断点信息传给它。这就是声明式UI和响应式设计的威力。
官方文档"组件布局场景"中对 Navigation 的 mode、navBarWidth 属性有详细说明。

实战二:视频详情页,如何用 SideBarContainer 实现"横屏看视频,竖屏看评论"?

很多视频App,竖屏时视频在上,评论在下;横屏或大屏时,希望视频在左,评论在右侧边栏。
传统if-else做法: 判断横竖屏,用不同的 Column 或 Row 包裹视频和评论组件,还要处理滑动冲突。
"一多"优雅做法: 使用 SideBarContainer 组件。

@Component
struct VideoDetailPage {
  @StorageLink('currentWidthBreakpoint') widthBp: string = 'sm';
  @State isSideBarShow: boolean = false; // 控制侧边栏显示

  build() {
    // 核心:SideBarContainer的type和showSideBar可以响应式改变
    SideBarContainer(
      this.widthBp === 'sm' ? SideBarContainerType.Overlay : SideBarContainerType.Embed
    ) {
      // 侧边栏内容:评论列表
      Column() {
        CommentList()
      }
      .sideBarWidth('30%') // 侧边栏宽度

      // 主内容区:视频播放器
      Column() {
        VideoPlayer()
      }
    }
    .showSideBar(this.isSideBarShow)
    .onChange((isShow: boolean) => {
      this.isSideBarShow = isShow; // 同步状态
    })
  }

  aboutToAppear() {
    // 大屏设备(md, lg)默认显示侧边栏
    if (this.widthBp !== 'sm') {
      this.isSideBarShow = true;
    }
  }
}

SideBarContainerType.Overlay 是覆盖式(像抽屉),Embed 是嵌入式(并排)。我们根据断点自动选择类型,并决定是否默认展开。代码清晰,意图明确。

SideBarContainer

Overlay模式

Embed模式

小屏/sm

大屏/md-lg

视频播放器

评论列表

覆盖显示

并排显示

实战三:商品列表页,如何根据屏幕宽度自动变2列、3列、4列?

这个需求太常见了。手机一列,小折叠屏展开可能两列,平板三列,PC四列。
传统if-else做法: 计算每项宽度,用 Flex 包裹,设置 wrap 并动态计算 justifyContent。
"一多"优雅做法: 使用 Grid 或 WaterFlow 组件,核心是 columnsTemplate 属性的响应式设置。
这里以 Grid 为例,展示如何结合文档中的 WidthBreakpointType 工具类:


// 假设有一个工具类,能根据当前宽度断点返回预设值
import { WidthBreakpointType } from '../common/WidthBreakpointUtil';

@Component
struct ProductGridPage {
  @StorageLink('widthBp') widthBp: WidthBreakpoint; // 假设备份是枚举

  build() {
    Grid() {
      ForEach(this.productList, (item: Product) => {
        GridItem() {
          ProductItemCard({ product: item })
        }
      })
    }
    // 魔法在这里:columnsTemplate 根据断点动态变化
    .columnsTemplate(
      `repeat(${
        new WidthBreakpointType(1, 2, 3, 4).getValue(this.widthBp)
      }, 1fr)`
    )
    .columnsGap(12)
    .rowsGap(12)
  }
}

new WidthBreakpointType(1, 2, 3, 4) 表示在 xs/sm/md/lg/xl 断点下,分别返回 1, 2, 3, 4。1fr 是栅格单位,表示均分。一行配置,搞定所有屏幕的列数适配。 WaterFlow 组件同理,使用 columnsTemplate。
官方文档"页面布局场景"中的"网格布局"、"瀑布流布局"示例,正是这个思路。

实战四:顶部搜索栏,如何在窄屏下折行,宽屏下水平排列?

有时,顶部有Logo、搜索框、用户头像几个元素。在手机上需要折行显示,在平板上可以水平排开。
传统if-else做法: 又是判断宽度,动态切换 Flex 的 direction 为 Column 或 Row。
"一多"优雅做法: 使用 栅格系统 GridRow / GridCol,这是响应式布局的瑞士军刀。

@Component
struct HeaderBar {
  @StorageLink('widthBp') widthBp: WidthBreakpoint;

  build() {
    // 定义栅格容器:不同断点下总列数不同
    GridRow({
      columns: { sm: 4, md: 8, lg: 12 },
      breakpoints: { value: ['320vp', '600vp', '840vp'] }
    }) {
      // Logo:在sm下占满4列(一行),在md/lg下占2列
      GridCol({ span: { sm: 4, md: 2, lg: 2 } }) {
        Image($r('app.media.logo'))
      }

      // 搜索框:在sm下占满4列(换行),在md/lg下占6列
      GridCol({
        span: { sm: 4, md: 6, lg: 6 },
        offset: { sm: 0, md: 0, lg: 1 } // lg下向右偏移1列
      }) {
        SearchInput()
      }

      // 用户头像:在sm下占满4列(换行),在md/lg下占2列
      GridCol({
        span: { sm: 4, md: 2, lg: 2 },
        offset: { sm: 0, md: 0, lg: 1 } // lg下再偏移1列
      }) {
        UserAvatar()
      }
    }
    .width('100%')
    .padding(12)
  }
}

通过为每个 GridCol 子组件在不同断点下配置不同的 span(占据列数)和 offset(偏移列数),元素会自动根据栅格规则进行折行或水平排列,无需任何条件判断。 这是最纯粹、最强大的响应式布局实现方式。
官方文档"响应式布局-栅格"章节有大量 span、offset、order 的示例,是必读内容。

锦上添花:让UI元素"听话"的7种自适应布局能力

解决了整体结构(响应式),我们还需要微调内部元素(自适应)。官方提炼的7种能力,我帮你白话一下:

  1. 拉伸:flexGrow/flexShrink。空间多时我多吃,空间少时我瘦身。
  2. 均分:justifyContent: SpaceEvenly。兄弟姐妹,把多余空间匀匀。
  3. 占比:width(‘50%’) 或 layoutWeight(1)。我就要占爸爸的一半/一份。
  4. 缩放:aspectRatio(1)。我宽高比锁死,要变一起变。
  5. 延伸:List 或 Scroll。内容太多?给我个滚动条接着展示。
  6. 隐藏:displayPriority(1)。空间不够?优先级低的兄弟先躲躲。
  7. 折行:FlexWrap.Wrap。一行站不下?自动换行。

核心思想:用属性声明你的意图,让框架去计算如何实现。 而不是你自己去算 left、top、width。

重构对比:if-else vs 声明式

我们最后看一个对比。假设有一个"新闻卡片"列表,需要适配多列。

if-else版本 (约30行,难以维护):

@Component struct NewsListOld {
  @State itemWidth: number = 0;
  aboutToAppear() {
    // 需要获取窗口宽度,计算边距和列数...
    let colCount = (windowWidth > 840) ? 3 : (windowWidth > 600) ? 2 : 1;
    this.itemWidth = (windowWidth - margin * (colCount + 1)) / colCount;
  }
  build() {
    Flex({ wrap: FlexWrap.Wrap }) {
      ForEach(this.news, item => {
        Column() {
          // ...
        }
        .width(this.itemWidth) // 动态计算宽度
        .margin(margin)
      })
    }
  }
}

声明式响应式版本 (约15行,意图清晰):

@Component struct NewsListNew {
  @StorageLink('widthBp') widthBp: WidthBreakpoint;
  build() {
    Grid() {
      ForEach(this.news, item => {
        GridItem() {
          NewsCard({ news: item })
        }
      })
    }
    .columnsTemplate(`repeat(${new WidthBreakpointType(1, 2, 3, 3).getValue(this.widthBp)}, 1fr)`)
    .columnsGap(12)
    .rowsGap(12)
  }
}

代码量减半,可读性、可维护性、可扩展性(比如未来加个xl断点4列)天差地别。

结语:从"工匠"到"架构师"

多端界面开发,不再是苦力活。鸿蒙的ArkUI通过 声明式语法 + 响应式组件 + 自适应能力,已经为我们铺好了一条康庄大道。
你的角色,应该从埋头计算尺寸的"代码工匠",转变为思考如何用最合适的"积木"(组件)和"拼法"(属性)来构建灵活、健壮界面的"架构师"。
真正决定你开发效率的,不是敲代码的手速,而是你选择的工具和设计思路。希望这篇文章,能帮你扔掉那些 if-else 小木块,开始享受用"乐高"搭应用的乐趣。
下一期,我们将进入更底层的 “功能开发” 领域。你会看到,在"一多"的世界里,功能开发的核心矛盾不再是业务逻辑,而是 如何优雅地处理不同设备间"有"与"无"、"强"与"弱"的硬件与系统能力差异。比如,手机有NFC而平板没有怎么办?不同设备的相机API调用有何不同?
预告:《功能模块的多端开发,你还在担心API不兼容?用SysCap机制,绕开"硬件级"的坑!》
我是你的技术老哥们儿,咱们下期再深聊。有任何布局上的疑惑,欢迎随时交流。

官方文档:多设备界面开发

相关文章:

Logo

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

更多推荐