鸿蒙应用开发UI基础第十节:线性布局进阶自适应约束实战演示

【学习目标】

  1. 理解 aspectRatio 宽高比约束的计算规则,实现组件等比缩放;
  2. 掌握"占比适配"的两种核心方式(百分比/layoutWeight)及优先级规则;
  3. 掌握 displayPriority 实现子组件按优先级显隐,适配容器尺寸变化;
  4. 掌握 Blank 组件实现主轴剩余空间自适应拉伸,适配多设备布局。

一、工程结构

1.1 新增演示文件

基于上一节 ColumnRowApplication 工程基础,在 entry/src/main/ets/pages 目录下新增4个页面:

复制代码
pages
├── AspectRatioPage.ets       # 宽高比约束演示
├── LayoutWeightScalePage.ets # 占比适配演示
├── DisplayPriorityPage.ets   # 显隐控制演示
├── BlankStretchPage.ets      # Blank自适应拉伸演示

1.2 更新主入口(Index.ets)

javascript 复制代码
// 引入路由模块(确保能正常跳转页面)
import router from '@ohos.router';

// 定义按钮数据的接口(规范数据结构)
interface ButtonItem {
  // 按钮显示文本
  title: string;
  // 跳转路由地址
  routerPath: string;
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';

  // 定义按钮数据源(统一管理所有按钮的文本和路由)
  private buttonList: ButtonItem[] = [
    // 基础布局
    { title: "横向布局主轴", routerPath: "pages/RowFlexAlignPage" },
    { title: "横向布局交叉轴", routerPath: "pages/RowItemAlignPage" },
    { title: "纵向布局主轴", routerPath: "pages/ColumnFlexAlignPage" },
    { title: "纵向布局交叉轴", routerPath: "pages/ColumnItemAlignPage" },
    { title: "子组件差异化对齐(alignSelf)", routerPath: "pages/AlignSelfPage" },
    { title: "Margin不生效问题演示以及解决方案", routerPath: "pages/MarginPage" },
    // 第十节进阶内容
    { title: "宽高比约束(aspectRatio)", routerPath: "pages/AspectRatioPage" },
    { title: "占比适配(百分比/layoutWeight)", routerPath: "pages/LayoutWeightScalePage" },
    { title: "显隐控制(displayPriority)", routerPath: "pages/DisplayPriorityPage" },
    { title: "自适应拉伸(Blank)", routerPath: "pages/BlankStretchPage" }
  ];

  build() {
    Column({ space: 20 }) {
      // 页面标题
      Text("线性布局演示")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .margin({ bottom: 30 })

      // 动态生成按钮列表 ForEach 循环遍历生成按钮
      ForEach(
        // 1. 数据源:按钮数组
        this.buttonList,
        // 2. 渲染函数:遍历每个元素生成Button
        (item: ButtonItem) => {
          Button(item.title)
            .width('80%') // 增加按钮宽度,适配长文本
            .fontSize(16) // 调整字体大小,避免文本溢出
            .onClick(() => {
              router.pushUrl({ url: item.routerPath });
            });
        },
        // 3. 唯一标识:保证列表更新时的性能(用路由地址作为唯一key)
        (item: ButtonItem) => item.routerPath
      )
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor($r('app.color.bg_page'));
  }
}

1.3 资源说明

本章节使用的图片资源均存放于项目 src/main/resources/base/media 目录下,配套代码已包含所有所需资源,无需额外配置。

二、宽高比约束:aspectRatio

aspectRatio 用于指定组件宽高比(计算公式:aspectRatio = 宽度/高度),可实现组件在不同尺寸容器下的等比缩放,是适配图片、视频封面、头像等场景的核心属性。

2.1 核心规则

  1. 优先级:aspectRatio > 手动设置的单维度尺寸(width/height);
  2. 计算逻辑(父容器空间足够时):
    • 仅设置宽度 + aspectRatio → 高度 = 宽度 / aspectRatio;
    • 仅设置高度 + aspectRatio → 宽度 = 高度 × aspectRatio;
    • 同时设置宽高 + aspectRatio → height 失效,高度强制按"宽度/aspectRatio"计算;
  3. 边界限制:组件最终尺寸受父容器内容区大小约束,constraintSize 优先级高于 aspectRatio
  4. 优先级层级:constraintSize(容器约束) > aspectRatio > 手动设置的单维度尺寸(width/height)。

2.2 宽高比示例代码(aspectRatio)

javascript 复制代码
@Entry
@Component
struct AspectRatioPage {
  build() {
    Column({ space: 20 }) {
      // 页面标题
      Text("宽高比约束演示(aspectRatio)")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')
        .textAlign(TextAlign.Center);

      // 场景1:16:9 宽高比(视频封面)
      Text("场景1:16:9 宽高比(宽度固定,高度自动计算)")
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .margin({ bottom: 5 });

      Text("视频封面")
        .width('90%')
        .aspectRatio(16/9)
        .backgroundColor($r('app.color.bg_blue_light'))
        .fontColor($r('app.color.text_white'))
        .textAlign(TextAlign.Center)
        .borderRadius(8)
        .padding(10);

      // 场景2:1:1 宽高比(圆形头像)
      Text("场景2:1:1 宽高比(高度固定,宽度自动=高度)")
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .margin({ top: 20, bottom: 5 });

      Text("头像")
        .height(100)
        .aspectRatio(1)
        .backgroundColor($r('app.color.bg_red_light'))
        .fontColor($r('app.color.text_white'))
        .textAlign(TextAlign.Center)
        .borderRadius(50)
    }
    .width('100%')
    .height('100%')
    .padding(15)
    .backgroundColor($r('app.color.bg_page'));
  }
}

2.3 运行效果说明

  • 16:9 视频封面:宽度占屏幕90%,高度自动计算为 宽度 ÷ (16/9),始终保持16:9比例;
  • 1:1 圆形头像:高度固定100vp,宽度自动等于高度(100vp),结合 borderRadius(50) 实现完美圆形。

三、占比适配:百分比 / layoutWeight

线性布局中实现"按比例分配空间"有两种核心方式,适用于不同场景:

3.1 核心规则对比

适配方式 参考基准 生效方向 核心特点 适用场景
百分比 父/祖先容器的实际宽/高 任意方向 直接按比例适配,计算简单 固定比例的静态布局(如分栏、按钮宽度)
layoutWeight 父容器主轴剩余空间 仅在主轴方向分配剩余空间 按权重分配空间,适配性更强 动态布局(如内容区+操作区、列表项分配)

layoutWeight 计算公式:

组件最终尺寸 = 父容器主轴剩余空间 × (自身 layoutWeight 值 / 所有子组件 layoutWeight 之和)

示例:父容器高度200vp,子组件 layoutWeight 为2和1 → 剩余空间200vp,内容区=200×(2/3)≈133vp,操作区=200×(1/3)≈67vp。

注意:使用 layoutWeight 时,子组件不要手动设置主轴方向的固定尺寸(如Row中不设width、Column中不设height),否则权重分配规则会失效。

3.2 代码演示:LayoutWeightScalePage.ets

javascript 复制代码
@Entry
@Component
struct LayoutWeightScalePage {
  build() {
    Column({ space: 20 }) {
      // 页面标题
      Text("占比适配演示(百分比/layoutWeight)")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')
        .textAlign(TextAlign.Center);

      // 场景1:百分比占比
      Text("场景1:百分比占比(宽度50%)")
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .margin({ bottom: 5 });
      
      Row() {
        Text("宽度50%")
          .width('50%')
          .height(60)
          .backgroundColor($r('app.color.bg_red_light'))
          .fontColor($r('app.color.text_white'))
          .textAlign(TextAlign.Center)
        
        Text("剩余50%")
          .width('50%')
          .height(60)
          .backgroundColor($r('app.color.bg_blue_light'))
          .fontColor($r('app.color.text_white'))
          .textAlign(TextAlign.Center)
      }
      .width('90%')
      .backgroundColor($r('app.color.bg_white'))
      .borderRadius(8);

      // 场景2:layoutWeight 权重占比
      Text("场景2:layoutWeight 2:1 分配(主轴剩余空间)")
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .margin({ top: 20, bottom: 5 });
      
      Column({ space: 0 }) {
        Text("内容区(2/3)")
          .layoutWeight(2)
          .width('100%')
          .backgroundColor($r('app.color.bg_purple_light'))
          .fontColor($r('app.color.text_white'))
          .textAlign(TextAlign.Center)
        
        Text("操作区(1/3)")
          .layoutWeight(1)
          .width('100%')
          .backgroundColor($r('app.color.bg_orange_light'))
          .fontColor($r('app.color.text_white'))
          .textAlign(TextAlign.Center)
      }
      .width('90%')
      .height(200)
      .borderRadius(8);
    }
    .width('100%')
    .height('100%')
    .padding(15)
    .backgroundColor($r('app.color.bg_page'));
  }
}

3.3 运行效果说明

  • 百分比占比:两个文本组件各占Row宽度的50%,比例固定,无论Row整体宽度如何变化,均保持1:1;
  • layoutWeight权重占比:Column总高度200vp,内容区和操作区按2:1分配高度(内容区≈133vp,操作区≈67vp),无需手动计算具体数值,适配更灵活。

四、显隐控制:displayPriority

displayPriority 是线性布局中适配容器尺寸变化的核心属性,用于控制"空间不足时子组件的隐藏顺序",解决多设备适配时"内容挤爆容器"的问题。

4.1 核心规则

  1. 优先级逻辑:值越小 → 重要性越低 → 空间不足时越先被移除;值越大 → 重要性越高 → 空间不足时越晚被移除;
  2. 移除行为:组件被隐藏时直接从组件节点移除,无文本压缩、省略号等过渡效果;
  3. 触发条件:Row/Column 默认不换行,容器宽度/高度不足时自动触发。

4.2 代码演示:DisplayPriorityPage.ets

javascript 复制代码
@Entry
@Component
struct DisplayPriorityPage {
  @State containerWidth: number = 375; // 手机基准宽度(375vp)
  @State isPlaying: boolean = false;

  build() {
    Column({ space: 20 }) {
      // 页面标题
      Text("displayPriority 音乐控制栏演示")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')
        .textAlign(TextAlign.Center);

      // 资源说明:以下图片资源均存放于项目的 src/main/resources/base/media 目录下,配套代码已包含完整资源文件
      // 核心演示:音乐控制栏(值越小重要性越低,宽度不足时先被移除)
      Row({space:20}) {
        // 重要性1(最低):随机播放 → 最先被移除(非核心功能)
        Image($r('app.media.ic_public_random'))
          .objectFit(ImageFit.Contain) // 保证图片等比显示,不变形
          .width(25)
          .height(25)
          .displayPriority(1);

        // 重要性2:单曲循环 → 第二步被移除(次要功能)
        Image($r('app.media.ic_public_single_cycle'))
          .objectFit(ImageFit.Contain)
          .width(25)
          .height(25)
          .displayPriority(2);

        // 重要性3:上一曲 → 第三步被移除(常用功能)
        Image($r('app.media.ic_previous'))
          .objectFit(ImageFit.Contain)
          .width(25)
          .height(25)
          .displayPriority(3);

        // 重要性4(最高):播放/暂停 → 最后被移除(核心功能)
        Image(this.isPlaying ? $r('app.media.ic_pause') : $r('app.media.ic_play'))
          .objectFit(ImageFit.Contain)
          .width(36)
          .height(36)
          .displayPriority(4)
          .onClick(() => this.isPlaying = !this.isPlaying);

        // 重要性3:下一曲 → 第三步被移除(常用功能)
        Image($r('app.media.ic_next'))
          .objectFit(ImageFit.Contain) // 补充缺失的图片适配属性
          .width(25)
          .height(25)
          .displayPriority(3);

        // 重要性2:列表循环 → 第二步被移除(次要功能)
        Image($r('app.media.ic_public_list_cycle'))
          .objectFit(ImageFit.Contain)
          .width(25)
          .height(25)
          .displayPriority(2);

        // 重要性1(最低):收藏 → 最先被移除(非核心功能)
        Image($r('app.media.ic_public_favor_filled'))
          .objectFit(ImageFit.Contain)
          .width(25)
          .height(25)
          .displayPriority(1);
      }
      .padding(15)
      .width(Math.round(this.containerWidth))
      .backgroundColor($r('app.color.bg_white'))
      .justifyContent(FlexAlign.Center)

      // 滑动条调节区域(模拟不同设备宽度)
      Column({ space: 10 }) {
        Slider({
          value: this.containerWidth,
          min: 150,    // 手表宽度
          max: 375,    // 手机宽度
          step: 1,
          style: SliderStyle.OutSet
        })
          .width('90%')
          .onChange((value: number) => {
            this.containerWidth = value;
          });

        Text(`当前容器宽度:${Math.round(this.containerWidth)}vp`)
          .fontSize(16)
          .fontColor($r('app.color.text_secondary'))
          .width('90%')
          .textAlign(TextAlign.Center);
      }

      // 核心提示(精准说明优先级规则)
      Text("核心逻辑:displayPriority值越小重要性越低,宽度不足时小数值按钮先从组件节点移除")
        .fontSize(12)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .textAlign(TextAlign.Center);
    }
    .justifyContent(FlexAlign.SpaceEvenly)
    .width('100%')
    .height('100%')
    .padding(15)
    .backgroundColor($r('app.color.bg_page'));
  }
}

4.3 运行效果

4.4 displayPriority显隐规则

displayPriority仅在 Row、Column或单行 Flex容器组件中生效:

  • 所有子组件的 displayPriority值均 ≤1 时,优先级无区别(所有组件均显示);
  • 当子组件的值 >1 时,数值越大优先级越高;
  • 未显式设置时,系统自动采用默认值 1;
  • 嵌套容器中,仅最外层容器的 displayPriority 会被系统识别(决定整个嵌套容器的移除时机);
  • 外层容器被移除时,内部所有子组件(无论自身优先级)都会被一并从组件节点移除;
  • 内层容器的 displayPriority 完全不生效,即便设置也不会改变移除逻辑;

示例代码:

javascript 复制代码
Row() {
  // 外层Row的displayPriority决定整个嵌套容器的移除时机,默认值1
  Row(){
    Text("元素1").width(60).displayPriority(1); // 内部优先级无影响
    Text("元素2").width(60).displayPriority(3);
  }

  Text("元素3")
    .width(60)
    .displayPriority(3);

  Row(){
    Text("元素4").width(60).displayPriority(4);
    Text("元素5").width(60).displayPriority(5);
  }
  .displayPriority(2);

}.width(Math.round(this.containerWidth))

运行效果

父组件Row宽度由大变小过程,元素1``元素2组件被外层Row包裹,受外层Row的displayPriority等级影响(未显式设置则默认值为1),他们一起先消失,displayPriority(2)其次,最后displayPriority(3)。

五、自适应拉伸:Blank 组件

Blank 是线性布局的"空白填充神器",用于在主轴方向自动填充剩余空间,实现"固定元素+自适应空白"的布局效果。

5.1 核心特性

  1. 生效范围:仅在 Row/Column/Flex 布局中生效;
  2. 核心作用:自动填充父容器主轴方向的剩余空间;
  3. 扩展用法:可通过 min 参数设置最小填充尺寸(如 Blank(20));
  4. 单位支持:数字默认vp,字符串可指定单位(如 Blank('20vp'),数字默认使用vp单位)。

5.2 代码演示:BlankStretchPage.ets

javascript 复制代码
@Entry
@Component
struct BlankStretchPage {
  build() {
    Column({ space: 15 }) {
      // 页面标题
      Text("Blank 自适应拉伸演示")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor($r('app.color.text_primary'))
        .width('100%')
        .textAlign(TextAlign.Center);

      // 场景1:Row 左/右固定 + 中间填充(常用导航栏)
      Text("场景1:左侧固定 + 右侧固定 + 中间填充")
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .margin({ bottom: 5 });
      Row({ space: 10 }) {
        Text("返回")
          .fontSize(16)
          .fontColor($r('app.color.text_primary'))
          .padding(8);
        Blank(); // 填充Row主轴剩余空间,实现"返回左对齐、保存右对齐"
        Button("保存")
          .fontSize(14)
          .padding({ left: 15, right: 15 })
          .backgroundColor($r('app.color.primary_blue'))
          .fontColor($r('app.color.text_white'))
          .borderRadius(6);
      }
      .width('90%')
      .padding(10)
      .backgroundColor($r('app.color.bg_white'))
      .borderRadius(8)
      .shadow({ radius: 2, color: '#EEEEEE', offsetX: 0, offsetY: 2 });

      // 场景2:Column 上下固定 + 中间填充(常用弹窗)
      Text("场景2:顶部固定 + 底部固定 + 中间填充")
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .margin({ top: 15, bottom: 5 });
      Column({ space: 10 }) {
        Text("标题栏")
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .padding(8);
        Blank(); // 填充Column主轴剩余空间,撑满弹窗高度
        Row({ space: 10 }) {
          Button("取消")
            .fontSize(14)
            .padding({ left: 15, right: 15 })
            .backgroundColor($r('app.color.bg_white'))
            .fontColor($r('app.color.text_primary'))
            .borderRadius(6);
          Button("确认")
            .fontSize(14)
            .padding({ left: 15, right: 15 })
            .backgroundColor($r('app.color.primary_blue'))
            .fontColor($r('app.color.text_white'))
            .borderRadius(6);
       }.justifyContent(FlexAlign.SpaceAround)
        .width('100%')
        
      }
      .width('90%')
      .height(200)
      .padding(10)
      .backgroundColor($r('app.color.bg_blue_light'))
      .borderRadius(8);

      // 场景3:多个Blank均分剩余空间
      Text("场景3:多个Blank均分剩余空间")
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
        .width('90%')
        .margin({ top: 15, bottom: 5 });
      Row({ space: 5 }) {
        Text("A").padding(8).backgroundColor($r('app.color.bg_red_light'));
        Blank(); // 第一个Blank
        Text("B").padding(8).backgroundColor($r('app.color.bg_blue_light'));
        Blank(); // 第二个Blank
        Text("C").padding(8).backgroundColor($r('app.color.bg_purple_light'));
      }
      .width('90%')
      .padding(10)
      .backgroundColor($r('app.color.bg_white'))
      .borderRadius(8);
    }
    .width('100%')
    .height('100%')
    .padding(15)
    .backgroundColor($r('app.color.bg_page'));
  }
}

5.3 运行效果说明

  • Row场景(导航栏):"返回"固定左对齐、"保存"固定右对齐,中间空白由Blank自动填充,适配不同宽度的屏幕;
  • Column场景(弹窗):顶部标题、底部操作区固定位置,中间Blank填充剩余高度,确保操作区始终在弹窗底部;
  • 多Blank场景:两个 Blank 会均分 Row 剩余宽度,实现 A、B、C 三个元素均匀分布。

六、核心注意事项

  1. aspectRatio:同时设置宽高会导致height失效,建议仅设置单维度+aspectRatio;
  2. layoutWeight:仅作用于主轴方向,交叉轴需用百分比/固定尺寸;
  3. displayPriority:嵌套容器仅外层优先级生效,被移除时直接从组件节点删除整个容器及内部组件;
  4. Blank仅在线性布局中生效,多个Blank共存时,会均分剩余空间。

七、内容总结

  1. aspectRatio 按宽高比自动计算组件尺寸,优先级高于手动单维度尺寸、低于constraintSize,实现等比缩放;
  2. 百分比适配基于父容器尺寸(任意方向),layoutWeight 按权重分配主轴剩余空间(仅主轴方向),可通过公式精准计算尺寸;
  3. displayPriority 数值越小重要性越低,空间不足时小数值组件先从组件节点移除,核心功能建议设大值;
  4. Blank 组件仅在线性布局中生效,用于填充主轴剩余空间,多个Blank共存时会均分剩余空间,实现元素对齐和自适应布局;
  5. displayPriority 触发移除时组件会直接从节点删除,无过渡效果。

八、代码仓库

九、下节预告

下一节我们将进入弹性布局 Flex 的核心学习,Flex 作为线性布局(Row/Column)的"进阶增强版",是鸿蒙应用多设备适配的核心布局方案。我们将重点掌握:

  1. Flex 与 Row/Column 的本质区别,理解主轴/交叉轴的灵活配置逻辑;
  2. flexGrow、flexShrink、flexBasis 三大核心属性的计算规则------对比本节 layoutWeight,掌握 Flex 更精细化的空间分配方式;
  3. 结合 wrap 换行规则,解决线性布局"空间不足只能隐藏/压缩"的局限性;
  4. 实战 Flex 对齐体系(justifyContent/alignItems/alignSelf),实现比线性布局更灵活的元素对齐;

通过本节学习,你将能从"固定维度适配"升级为"动态弹性适配",彻底解决不同屏幕尺寸下的布局适配难题。