[鸿蒙2025领航者闯关] 响应式布局与屏幕适配方案

问题描述

应用需要适配不同尺寸的设备(手机、平板、折叠屏),如何实现响应式布局?如何使用 layoutWeight 和 Grid 实现弹性布局?

关键字: 响应式布局、屏幕适配、layoutWeight、Grid 布局、多设备适配

解决方案

完整代码

复制代码
/**
 * 响应式统计卡片
 */
@Component
struct ResponsiveStatsCard {
  @State stats: Array<{title: string, value: string, color: string}> = [
    { title: '总收入', value: '¥5000', color: '#ff6b6b' },
    { title: '总支出', value: '¥3000', color: '#67c23a' },
    { title: '净收益', value: '¥2000', color: '#409eff' },
    { title: '记录数', value: '50笔', color: '#e6a23c' }
  ];
  
  build() {
    // 使用Grid实现响应式布局
    Grid() {
      ForEach(this.stats, (stat: any) => {
        GridItem() {
          Column({ space: 8 }) {
            Text(stat.value)
              .fontSize(24)
              .fontWeight(FontWeight.Bold)
              .fontColor(stat.color)
            Text(stat.title)
              .fontSize(14)
              .fontColor('#999')
          }
          .width('100%')
          .padding(20)
          .backgroundColor(Color.White)
          .borderRadius(12)
        }
      })
    }
    .columnsTemplate('1fr 1fr') // 2列布局
    .rowsGap(12)
    .columnsGap(12)
    .width('100%')
    .padding(16)
  }
}
​
/**
 * 弹性权重布局
 */
@Component
struct FlexWeightLayout {
  build() {
    Row() {
      // 左侧固定宽度
      Column() {
        Text('固定区域')
          .fontSize(16)
      }
      .width(100)
      .height('100%')
      .backgroundColor('#e3f2fd')
      
      // 中间弹性区域
      Column() {
        Text('弹性区域')
          .fontSize(16)
      }
      .layoutWeight(1) // 占据剩余空间
      .height('100%')
      .backgroundColor('#fff3e0')
      
      // 右侧固定宽度
      Column() {
        Text('固定区域')
          .fontSize(16)
      }
      .width(100)
      .height('100%')
      .backgroundColor('#f3e5f5')
    }
    .width('100%')
    .height(200)
  }
}
​
/**
 * 媒体查询适配
 */
import { mediaQuery } from '@kit.ArkUI';
​
@Entry
@Component
struct AdaptiveLayout {
  @State isTablet: boolean = false;
  private listener: mediaQuery.MediaQueryListener = mediaQuery.matchMediaSync('(min-width: 600vp)');
  
  aboutToAppear() {
    this.listener.on('change', (result: mediaQuery.MediaQueryResult) => {
      this.isTablet = result.matches;
    });
    this.isTablet = this.listener.matches;
  }
  
  aboutToDisappear() {
    this.listener.off('change');
  }
  
  build() {
    Column() {
      if (this.isTablet) {
        // 平板布局:左右分栏
        Row() {
          // 左侧列表
          List() {
            ForEach([1,2,3,4,5], (item: number) => {
              ListItem() {
                Text(`项目 ${item}`)
                  .width('100%')
                  .padding(16)
              }
            })
          }
          .width('40%')
          .backgroundColor('#f5f5f5')
          
          // 右侧详情
          Column() {
            Text('详情内容')
              .fontSize(20)
          }
          .layoutWeight(1)
          .backgroundColor(Color.White)
        }
        .width('100%')
        .height('100%')
      } else {
        // 手机布局:单列
        List() {
          ForEach([1,2,3,4,5], (item: number) => {
            ListItem() {
              Text(`项目 ${item}`)
                .width('100%')
                .padding(16)
            }
          })
        }
        .width('100%')
        .height('100%')
      }
    }
  }
}
​
/**
 * GridRow响应式布局
 */
@Component
struct GridRowDemo {
  build() {
    GridRow({
      columns: { sm: 4, md: 8, lg: 12 }, // 不同断点的列数
      gutter: { x: 12, y: 12 }, // 间距
      breakpoints: { value: ['320vp', '600vp', '840vp'] } // 断点
    }) {
      // 小屏占4列,中屏占4列,大屏占3列
      GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
        Column() {
          Text('卡片1')
        }
        .width('100%')
        .height(100)
        .backgroundColor('#e3f2fd')
        .borderRadius(8)
      }
      
      GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
        Column() {
          Text('卡片2')
        }
        .width('100%')
        .height(100)
        .backgroundColor('#fff3e0')
        .borderRadius(8)
      }
      
      GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
        Column() {
          Text('卡片3')
        }
        .width('100%')
        .height(100)
        .backgroundColor('#f3e5f5')
        .borderRadius(8)
      }
      
      GridCol({ span: { sm: 4, md: 4, lg: 3 } }) {
        Column() {
          Text('卡片4')
        }
        .width('100%')
        .height(100)
        .backgroundColor('#e0f7fa')
        .borderRadius(8)
      }
    }
    .width('100%')
    .padding(16)
  }
}

原理解析

1. layoutWeight 弹性布局

复制代码
.layoutWeight(1) // 占据剩余空间
  • 多个组件设置 layoutWeight 会按比例分配
  • 常用于左右分栏、上下分栏

2. Grid 网格布局

复制代码
.columnsTemplate('1fr 1fr') // 2列等宽
.columnsTemplate('100px 1fr 2fr') // 固定+弹性
  • fr 单位表示剩余空间的份数
  • 支持固定宽度和弹性宽度混合

3. 媒体查询

复制代码
mediaQuery.matchMediaSync('(min-width: 600vp)')
  • 监听屏幕宽度变化
  • 根据断点切换布局

4. GridRow 响应式

复制代码
columns: { sm: 4, md: 8, lg: 12 }
  • 自动根据屏幕宽度选择列数
  • span 设置占据的列数

最佳实践

  1. 优先使用 layoutWeight: 简单场景用 layoutWeight
  2. Grid 适合卡片: 统计卡片、功能入口用 Grid
  3. 媒体查询: 大幅布局变化用媒体查询
  4. GridRow: 复杂响应式用 GridRow
  5. 断点设置: 320vp(手机)、600vp(平板)、840vp(PC)

避坑指南

  1. layoutWeight 父容器: 父容器必须有明确宽度/高度
  2. Grid 高度: Grid 需要设置高度或使用 layoutWeight
  3. 媒体查询内存: 组件销毁时 off 监听
  4. GridRow 嵌套: 避免 GridRow 嵌套 GridRow
  5. 性能: 媒体查询变化时避免大量重绘

效果展示

  • 手机(320-600vp):单列布局,卡片占满宽
  • 平板(600-840vp):双列布局,左右分栏
  • PC(>840vp):多列布局,最大化利用空间
    相关资源
相关推荐
waeng_luo2 小时前
[鸿蒙2025领航者闯关] 表单验证与用户输入处理最佳实践
开发语言·前端·鸿蒙·鸿蒙2025领航者闯关·鸿蒙6实战·开发者年度总结
我是华为OD~HR~栗栗呀2 小时前
华为OD-C面经-23届学院哦
java·c++·python·华为od·华为·面试
kirk_wang4 小时前
Flutter Image Editor 适配鸿蒙HarmonyOS平台实践
flutter·华为·harmonyos
m0_685535085 小时前
大疆光学工程师面试经验
华为·光学·光学设计·光学工程·镜头设计
fatiaozhang95276 小时前
湖北联通华为悦盒EC6109-U_海思MV200_TTL烧录固件包
华为·电视盒子·刷机固件·机顶盒刷机·华为悦盒ec6109-u
国霄6 小时前
(7)Kotlin/Js For Harmony——ArkTs 开发架构
harmonyos
盐焗西兰花6 小时前
鸿蒙学习实战之路:Dialog 组件封装最佳实践
学习·华为·harmonyos
q_30238195566 小时前
华为Atlas310意图识别如何实现?
华为·自然语言处理·bert
大雷神6 小时前
HarmonyOS中高德地图第一篇:高德地图SDK集成与初始化
harmonyos