这是一个使用鸿蒙技术开发的本地原生记账应用,非常适合大家用来练手。相关源码已上传至 Github,点击此处查看项目。欢迎大家交流、指正,也欢迎提交 PR。

一、引言

在鸿蒙生态中,应用需要运行在手机、平板、折叠屏、智能手表、车机等多种设备上。这些设备的屏幕尺寸、分辨率、形态差异巨大。为了实现“一次开发,多端部署”的目标,HarmonyOS 提供了强大的自适应布局能力。本文将深入浅出地介绍什么是多端适配,以及如何使用鸿蒙应用开发中的七种自适应布局能力,构建出在不同屏幕上都表现良好的应用界面。

二、什么是“一次开发,多端适配”?

“一次开发,多端适配”并非指编写一套完全不变的代码,而是指开发者通过一套代码工程,在不同的设备上呈现出符合该设备屏幕特性和交互习惯的用户界面。其核心思想是:

  • 逻辑归一:使用统一的开发语言(ArkTS)和声明式 UI 框架(ArkUI),编写核心业务逻辑和界面布局。
  • 布局自适应:界面元素能够根据容器(通常是屏幕或窗口)的尺寸变化,自动调整自身的大小、位置、排列方式,甚至显示或隐藏。
  • 资源差异化:针对不同屏幕密度和尺寸,提供差异化的图片、字体等资源文件,系统会自动加载最合适的资源。

简单来说,开发者只需关注一套代码逻辑,而界面如何在不同屏幕上“优雅地”展示,则由自适应布局机制和资源管理策略共同完成。

三、自适应布局的七种核心能力

HarmonyOS 提供了七种自适应布局能力,它们是实现多端适配的基石。这些能力可以独立使用,也可以灵活组合,以应对复杂的布局需求。

能力 使用场景 实现方式
拉伸能力 容器尺寸变化时,将增减的空间全部分配给指定区域。 Flex 布局的 flexGrowflexShrink 属性
均分能力 容器尺寸变化时,将增减的空间均匀分配给所有空白区域。 Row/Column/FlexjustifyContent 设为 FlexAlign.SpaceEvenly
占比能力 子组件宽高按预设比例随容器变化。 百分比宽高 或 layoutWeight 属性
缩放能力 子组件宽高按比例变化且保持宽高比不变。 aspectRatio 属性
延伸能力 子组件按顺序随容器尺寸变化显示或隐藏。 List 组件 或 Scroll 配合 Row/Column
隐藏能力 子组件按预设优先级随容器尺寸变化显示或隐藏。 displayPriority 属性
折行能力 子组件按顺序随容器尺寸变化折行排列。 Flexwrap 设为 FlexWrap.Wrap

四、实战:七种常用自适应布局能力演示

下面,我们通过一个完整的示例,来演示拉伸、均分、占比、缩放、延伸、隐藏和折行这七种最常用的自适应布局能力的具体用法。

4.1 示例代码

interface NavItem {
  icon: Resource
  label: string
  priority: number
}

interface TabItem {
  label: string
}

@Entry
@Component
struct AdaptiveLayoutFullDemo {
  // 导航项列表,用于演示隐藏能力
  @State navItems: NavItem[] = [
    { icon: $r('sys.media.ohos_ic_public_phone'), label: '首页', priority: 5 },
    { icon: $r('sys.media.ohos_ic_public_phone'), label: '分类', priority: 4 },
    { icon: $r('sys.media.ohos_ic_public_phone'), label: '购物车', priority: 3 },
    { icon: $r('sys.media.ohos_ic_public_phone'), label: '消息', priority: 2 },
    { icon: $r('sys.media.ohos_ic_public_phone'), label: '收藏', priority: 1 }
  ]

  // 卡片颜色列表,用于演示折行能力
  @State cardColors: string[] = [
    '#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A',
    '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
  ]

  // 标签页列表,用于演示延伸能力
  @State tabItems: TabItem[] = [
    { label: '推荐' },
    { label: '关注' },
    { label: '视频' },
    { label: '直播' },
    { label: '购物' },
    { label: '附近' },
    { label: '游戏' }
  ]

  @State selectedIndex: number = 0

  build() {
    Scroll() {
      Column({ space: 16 }) {
        // ========== 1. 拉伸能力 ==========
        this.StretchSection()

        // ========== 2. 均分能力 ==========
        this.EvenlySection()

        // ========== 3. 占比能力 ==========
        this.ProportionSection()

        // ========== 4. 缩放能力 ==========
        this.ScaleSection()

        // ========== 5. 延伸能力 ==========
        this.ExtensionSection()

        // ========== 6. 隐藏能力 ==========
        this.HideSection()

        // ========== 7. 折行能力 ==========
        this.WrapSection()
      }
      .padding(16)
      .backgroundColor('#F5F5F5')
    }
    .width('100%')
    .height('100%')
  }

  // ----- 1. 拉伸能力 -----
  @Builder
  StretchSection() {
    Column({ space: 8 }) {
      Text('1. 拉伸能力(flexGrow / flexShrink)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center }) {
        TextInput({ placeholder: '请输入搜索内容' })
          .flexGrow(1)
          .flexShrink(1)
          .height(40)
          .backgroundColor('#FFFFFF')
          .borderRadius(20)
          .padding({ left: 16, right: 16 })

        Button('搜索')
          .flexGrow(0)
          .flexShrink(0)
          .width(72)
          .height(40)
          .margin({ left: 8 })
          .borderRadius(20)
      }
      .width('100%')
    }
  }

  // ----- 2. 均分能力 -----
  @Builder
  EvenlySection() {
    Column({ space: 8 }) {
      Text('2. 均分能力(FlexAlign.SpaceEvenly)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      Row() {
        Button('功能A')
          .width(80)
          .height(36)
          .borderRadius(18)
        Button('功能B')
          .width(80)
          .height(36)
          .borderRadius(18)
        Button('功能C')
          .width(80)
          .height(36)
          .borderRadius(18)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
    }
  }

  // ----- 3. 占比能力 -----
  @Builder
  ProportionSection() {
    Column({ space: 8 }) {
      Text('3. 占比能力(layoutWeight)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      Row() {
        Column() {
          Text('主内容区')
            .fontSize(14)
            .textAlign(TextAlign.Center)
        }
        .layoutWeight(2)
        .height(40)
        .backgroundColor('#E3F2FD')
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)

        Column() {
          Text('边栏')
            .fontSize(14)
            .textAlign(TextAlign.Center)
        }
        .layoutWeight(1)
        .height(40)
        .backgroundColor('#FFF3E0')
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)
        .margin({ left: 8 })
      }
      .width('100%')
    }
  }

  // ----- 4. 缩放能力 -----
  @Builder
  ScaleSection() {
    Column({ space: 8 }) {
      Text('4. 缩放能力(aspectRatio)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      Column() {
        Text('16:9 等比缩放区域 \n @程序员黑豆')
          .fontSize(14)
          .fontColor('#FFFFFF')
          .textAlign(TextAlign.Center)
      }
      .width('100%')
      .aspectRatio(16 / 9)
      .backgroundColor('#7986CB')
      .borderRadius(8)
      .justifyContent(FlexAlign.Center)
    }
  }

  // ----- 5. 延伸能力 -----
  @Builder
  ExtensionSection() {
    Column({ space: 8 }) {
      Text('5. 延伸能力(Scroll + Row)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      // 容器宽度充足时所有标签平铺展示;
      // 容器宽度不足时末尾标签被裁剪,可通过横向滚动访问。
      Scroll() {
        Row({ space: 24 }) {
          ForEach(this.tabItems, (item: TabItem, index: number) => {
            Text(item.label)
              .fontSize(16)
              .fontColor(this.selectedIndex === index ? '#007DFF' : '#666666')
              .fontWeight(this.selectedIndex === index ? FontWeight.Bold : FontWeight.Normal)
              .padding({ left: 12, right: 12, top: 8, bottom: 8 })
              .borderRadius(16)
              .backgroundColor(this.selectedIndex === index ? '#E3F2FD' : '#F5F5F5')
              .onClick(() => {
                this.selectedIndex = index
              })
          }, (item: TabItem, index: number) => `${index}_${item.label}`)
        }
        .padding({ left: 16, right: 16 })
      }
      .scrollable(ScrollDirection.Horizontal)
      .scrollBar(BarState.Off)
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .borderRadius(12)
    }
  }

  // ----- 6. 隐藏能力 -----
  @Builder
  HideSection() {
    Column({ space: 8 }) {
      Text('6. 隐藏能力(displayPriority)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      Row() {
        ForEach(this.navItems, (item: NavItem) => {
          Column({ space: 4 }) {
            Image(item.icon)
              .width(24)
              .height(24)
            Text(item.label)
              .fontSize(12)
          }
          .width(60)
          .height(60)
          .justifyContent(FlexAlign.Center)
          .displayPriority(item.priority)
        }, (item: NavItem) => item.label)
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceEvenly)
      .height(60)
      .backgroundColor('#FFFFFF')
      .borderRadius(8)
      .padding({ top: 8, bottom: 8 })
    }
  }

  // ----- 7. 折行能力 -----
  @Builder
  WrapSection() {
    Column({ space: 8 }) {
      Text('7. 折行能力(FlexWrap.Wrap)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)

      Flex({
        direction: FlexDirection.Row,
        wrap: FlexWrap.Wrap
      }) {
        ForEach(this.cardColors, (color: string, index: number) => {
          Column() {
            Text(`卡片${index + 1}`)
              .fontSize(13)
              .fontColor('#FFFFFF')
          }
          .width(72)
          .height(72)
          .backgroundColor(color)
          .borderRadius(8)
          .justifyContent(FlexAlign.Center)
          .margin(4)
        }, (color: string, index: number) => `${index}_${color}`)
      }
      .width('100%')
    }
  }
}

4.2 运行效果

请添加图片描述

4.3 代码说明

上述示例代码演示了七种自适应布局能力的核心用法,下面对关键部分进行说明:

  • 数据结构定义:代码开头定义了 NavItem(导航项,含 priority 优先级字段)和 TabItem(标签项)两个接口,为后续演示隐藏能力和延伸能力提供数据模型。
  • 状态管理:使用 @State 装饰器管理 navItemscardColorstabItemsselectedIndex 等响应式数据,当数据变化时 UI 自动更新。
  • @Builder 构建函数:将每种布局能力封装为独立的 @Builder 函数(如 StretchSectionEvenlySection),使 build() 方法结构清晰、易于维护。
  • Scroll 容器:最外层使用 Scroll 组件包裹所有布局演示区域,确保当内容超出屏幕高度时页面可纵向滚动,这是多端适配中处理内容溢出的基础手段。
  • 延伸能力实现:在 ExtensionSection 中,使用 Scroll 包裹 Row 实现横向滚动标签栏。当容器宽度充足时所有标签平铺展示;宽度不足时末尾标签被裁剪,用户可通过横向滑动访问隐藏的标签。
  • 隐藏能力实现:在 HideSection 中,通过 displayPriority(item.priority) 为每个导航项设置显示优先级。当容器空间不足时,priority 值最低的项会优先被隐藏,从而实现按需显示。
  • 折行能力实现:在 WrapSection 中,Flex 容器设置 wrap: FlexWrap.Wrap,当一行排满卡片后自动换行,无需手动计算换行逻辑。

五、关键注意事项与最佳实践

5.1 各能力的关键注意事项

  • 拉伸能力flexGrow 默认值为 0(不拉伸),flexShrink 默认值为 1(会收缩)。如果希望某个子组件尺寸完全固定,需同时设置 flexGrow(0)flexShrink(0)。该能力仅在 Flex 容器中生效,Row/Column 容器下不生效。
  • 均分能力FlexAlign.SpaceEvenly 将剩余空间均匀分配到所有间隔(包括首尾两侧),而 FlexAlign.SpaceBetween 只将剩余空间分配到子组件之间的间隔(首尾无间距),FlexAlign.SpaceAround 则是每个子组件两侧各分配一半间隔。三者效果不同,需根据设计需求选择。
  • 占比能力layoutWeight 与百分比宽高两种方式均可实现占比。layoutWeight 的优势在于按整数比例分配,适合需要精确比例控制的场景;百分比方式则更直观。注意同一容器中不要混用两种方式,可能导致布局计算不一致。
  • 缩放能力aspectRatio 接受一个正数表示宽高比(宽/高)。设置后只需指定宽或高其中一个维度,另一个维度会自动计算。适合图片、视频等需要保持比例的场景。
  • 隐藏能力displayPriority 的值越大表示显示优先级越高,当容器空间不足时,低优先级的子组件会优先被隐藏。需要注意被隐藏的组件仍占据布局计算过程,只是不渲染。此能力仅在 Row/Flex 等线性容器中生效。
  • 折行能力:当 Flex 设置 wrap(FlexWrap.Wrap) 后,主轴长度为 auto 的自适应布局会失效,默认撑满父容器。如果需要精确控制换行行为,可以通过 measureText 等方式计算子组件实际宽度,动态决定是否换行。

5.2 通用最佳实践

  1. 结合响应式布局:自适应布局解决的是窗口尺寸在一定范围内连续变化时的适配问题。当窗口尺寸跨越较大阈值(如从手机到平板)需要布局结构发生根本性变化时,应结合响应式布局(如 GridRow/GridCol、媒体查询、断点系统)共同使用。
  2. 避免固定像素值:优先使用 vp/fp 单位或百分比,以确保在不同屏幕密度下正确缩放。
  3. 多种能力叠加:多种自适应能力可以叠加使用,例如一个 Flex 容器中同时使用 flexGrow(拉伸)和 wrap(折行),但需注意叠加后的布局行为可能较为复杂,建议在真机上充分验证。

六、总结

通过掌握 HarmonyOS 提供的七种自适应布局能力,开发者可以高效地构建出能够适应多种屏幕尺寸的应用界面,真正实现“一次开发,多端适配”的目标。建议开发者在实际项目中多加练习,并结合响应式布局策略,打造出极致的多端用户体验。

Logo

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

更多推荐