鸿蒙开发之:ArkUI组件布局全解析

38 阅读8分钟

本文字数:3120字 | 预计阅读时间:12分钟

前置知识:建议先学习本系列前两篇《环境搭建与第一个应用》、《ArkTS语言基础入门》

实战价值:学完本文你将能够独立构建复杂鸿蒙应用界面

系列导航:本文是《鸿蒙开发系列》第3篇,下篇将深入讲解状态管理与数据绑定

一、ArkUI布局哲学:声明式UI vs 命令式UI 1.1 什么是声明式UI? 传统Android/iOS开发采用命令式方式:你需要告诉系统"如何做"(先创建View,再设置属性,最后添加到父容器)。而鸿蒙ArkUI采用声明式方式:你只需要描述界面"是什么样子",系统自动处理渲染和更新。

typescript

// 命令式UI(传统Android) val textView = TextView(context) textView.text = "Hello" textView.textSize = 20f layout.addView(textView)

// 声明式UI(鸿蒙ArkUI) Text('Hello') .fontSize(20) 1.2 ArkUI布局三要素 typescript

@Entry @Component struct LayoutDemo { build() { // 1. 布局容器:决定子组件的排列方式 Column() { // 2. UI组件:显示内容的元素 Text('Hello ArkUI')

  // 3. 装饰器:美化组件的样式
  .fontSize(20)
}

} } 二、基础组件深度解析 2.1 文本组件:不只是显示文字 typescript

Text('鸿蒙开发实战指南') // 字体相关 .fontSize(24) // 字号 .fontColor('#FF0000') // 字体颜色 .fontWeight(FontWeight.Bold) // 字重:Bold、Normal、Light等 .fontFamily('HarmonyOS Sans') // 字体家族 .fontStyle(FontStyle.Italic) // 斜体

// 对齐与布局 .textAlign(TextAlign.Center) // 对齐方式:Start、Center、End .maxLines(2) // 最大行数 .textOverflow({overflow: TextOverflow.Ellipsis}) // 溢出显示... .lineHeight(30) // 行高

// 装饰效果 .decoration({ type: TextDecorationType.Underline, // 下划线、上划线、删除线 color: Color.Blue }) .letterSpacing(2) // 字间距 .textCase(TextCase.Normal) // 大小写:Normal、UpperCase、LowerCase

// 布局约束 .width(200) // 宽度 .height(50) // 高度 .backgroundColor('#F5F5F5') // 背景色 .borderRadius(10) // 圆角 .padding(10) // 内边距

// 事件处理 .onClick(() => { console.log('文本被点击'); }) 2.2 按钮组件:交互的核心 typescript

Button('点击登录') // 尺寸与形状 .width('90%') // 百分比宽度 .height(56) // 固定高度 .size({ width: 200, height: 50 }) // 同时设置宽高 .borderRadius(28) // 圆角按钮

// 样式配置 .backgroundColor('#007DFF') .fontColor(Color.White) .fontSize(18) .fontWeight(FontWeight.Medium)

// 边框与阴影 .border({ width: 1, color: '#007DFF', style: BorderStyle.Solid }) .shadow({ radius: 10, color: '#33000000', offsetX: 0, offsetY: 4 })

// 状态样式 .stateEffect(true) // 开启按压效果 .enabled(this.isLoginEnabled) // 启用/禁用

// 点击事件 .onClick(() => { this.handleLogin(); }) 2.3 输入框组件:用户交互入口 typescript

TextInput({ placeholder: '请输入用户名' }) // 基础配置 .width('100%') .height(48) .fontSize(16) .type(InputType.Normal) // 输入类型:Normal、Number、Password等

// 样式配置 .backgroundColor('#FFFFFF') .borderRadius(8) .border({ width: 1, color: this.isInputError ? '#FF3B30' : '#E5E5E5' }) .padding({ left: 16, right: 16 })

// 输入限制 .maxLength(20) // 最大长度 .enterKeyType(EnterKeyType.Go) // 回车键类型

// 事件监听 .onChange((value: string) => { this.username = value; this.validateInput(); }) .onSubmit(() => { this.handleSubmit(); }) .onEditChange((isEditing: boolean) => { this.isInputFocus = isEditing; }) 2.4 图片组件:视觉呈现 typescript

// 加载网络图片 Image('example.com/image.jpg') .width(200) .height(200) .objectFit(ImageFit.Contain) // 适应方式:Contain、Cover、Fill等 .interpolation(ImageInterpolation.High) // 插值质量 .renderMode(ImageRenderMode.Original) // 渲染模式 .draggable(true) // 可拖拽

// 加载本地资源 Image($r('app.media.logo')) // 从resources资源加载 .width(100) .height(100)

// 图片加载状态处理 Image(this.imageUrl) .alt($r('app.media.placeholder')) // 加载失败时的占位图 .onComplete((event: { width: number, height: number }) => { console.log('图片加载完成'); }) .onError(() => { console.log('图片加载失败'); }) 三、六大布局容器详解 3.1 Column:垂直线性布局 typescript

Column({ space: 20 }) { // space:子组件垂直间距 Text('顶部标题') .fontSize(24) .fontWeight(FontWeight.Bold)

Text('这是描述内容...') .fontSize(16) .fontColor('#666666')

Button('操作按钮') .width(200) .height(50) } .justifyContent(FlexAlign.Start) // 垂直对齐方式 .alignItems(HorizontalAlign.Center) // 水平对齐方式 .width('100%') .height('100%') .padding(24) .backgroundColor('#FFFFFF') 3.2 Row:水平线性布局 typescript

Row({ space: 15 }) { // space:子组件水平间距 Image($r('app.media.avatar')) .width(40) .height(40) .borderRadius(20)

Column() { Text('张三') .fontSize(16) .fontWeight(FontWeight.Medium)

Text('华为开发者')
  .fontSize(12)
  .fontColor('#999999')

} .alignItems(HorizontalAlign.Start)

// 使用空白填充器实现右对齐 Blank()

Text('16:30') .fontSize(12) .fontColor('#999999') } .width('100%') .padding({ left: 16, right: 16, top: 12, bottom: 12 }) .alignItems(VerticalAlign.Center) 3.3 Stack:层叠布局 typescript

Stack({ alignContent: Alignment.TopStart }) { // 底层:背景图 Image($r('app.media.bg')) .width('100%') .height(200) .objectFit(ImageFit.Cover)

// 中层:渐变遮罩 Column() .width('100%') .height(200) .backgroundImage( 'linear-gradient(to bottom, transparent 0%, #00000080 100%)' )

// 上层:内容 Column({ space: 10 }) { Text('鸿蒙开发') .fontSize(24) .fontColor(Color.White)

Text('打造全场景智慧体验')
  .fontSize(14)
  .fontColor(Color.White)
  .opacity(0.8)

} .margin({ top: 100, left: 20 }) } .width('100%') .height(200) 3.4 Flex:弹性盒子布局 typescript

Flex({ direction: FlexDirection.Row, // 排列方向 wrap: FlexWrap.Wrap, // 是否换行 justifyContent: FlexAlign.SpaceBetween, // 主轴对齐 alignItems: ItemAlign.Center // 交叉轴对齐 }) { // 固定比例子项 Text('商品1').flexGrow(1) // flex-grow: 1 Text('商品2').flexGrow(2) // flex-grow: 2 Text('商品3').flexShrink(1) // flex-shrink: 1 Text('商品4').flexBasis('25%') // flex-basis: 25% } .width('100%') .padding(10) .backgroundColor('#F9F9F9') 3.5 Grid:网格布局 typescript

// 方式1:使用Grid容器 Grid() { ForEach(this.productList, (item: Product, index: number) => { GridItem() { Column({ space: 8 }) { Image(item.image) .width(80) .height(80) .objectFit(ImageFit.Cover)

    Text(item.name)
      .fontSize(14)
      .maxLines(2)
      .textOverflow({overflow: TextOverflow.Ellipsis})
  }
  .width(100)
  .height(130)
  .padding(10)
  .backgroundColor(Color.White)
  .borderRadius(8)
  .shadow({ radius: 4, color: '#10000000' })
}

}) } .columnsTemplate('1fr 1fr 1fr') // 3列等宽 .rowsTemplate('1fr 1fr') // 2行等高 .columnsGap(12) // 列间距 .rowsGap(16) // 行间距 .padding(16) .backgroundColor('#F5F5F5') 3.6 List:列表布局 typescript

List({ space: 10 }) { ForEach(this.messageList, (item: Message, index: number) => { ListItem() { Row({ space: 12 }) { // 头像 Image(item.avatar) .width(44) .height(44) .borderRadius(22)

    // 消息内容
    Column({ space: 4 }) {
      Row() {
        Text(item.sender)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
        
        Blank()
        
        Text(item.time)
          .fontSize(12)
          .fontColor('#999999')
      }
      .width('100%')
      
      Text(item.content)
        .fontSize(14)
        .fontColor('#666666')
        .maxLines(1)
        .textOverflow({overflow: TextOverflow.Ellipsis})
    }
    .alignItems(HorizontalAlign.Start)
    .flexGrow(1)
  }
  .width('100%')
  .padding({ top: 10, bottom: 10, left: 16, right: 16 })
  .backgroundColor(index === this.activeIndex ? '#F0F7FF' : Color.White)
}
.onClick(() => {
  this.activeIndex = index;
  this.viewMessage(item);
})

}) } .width('100%') .height('100%') .divider({ strokeWidth: 1, color: '#F0F0F0', startMargin: 70, endMargin: 16 }) 四、高级布局技巧 4.1 响应式布局 typescript

@Entry @Component struct ResponsiveLayout { @State currentBreakpoint: string = 'mobile';

// 监听窗口变化 aboutToAppear() { window.on('windowSizeChange', (data: { width: number, height: number }) => { if (data.width >= 1200) { this.currentBreakpoint = 'desktop'; } else if (data.width >= 768) { this.currentBreakpoint = 'tablet'; } else { this.currentBreakpoint = 'mobile'; } }); }

build() { Column() { if (this.currentBreakpoint === 'mobile') { // 移动端布局 this.buildMobileLayout(); } else if (this.currentBreakpoint === 'tablet') { // 平板布局 this.buildTabletLayout(); } else { // 桌面端布局 this.buildDesktopLayout(); } } .width('100%') .height('100%') }

// 移动端布局(单列) @Builder buildMobileLayout() { Column({ space: 20 }) { Text('移动端视图') // ... 其他组件 } .padding(16) }

// 平板布局(两列) @Builder buildTabletLayout() { Row({ space: 24 }) { Column({ space: 20 }) { Text('侧边栏') // ... 侧边栏组件 } .width('30%')

  Column({ space: 20 }) {
    Text('主内容')
    // ... 主要内容组件
  }
  .width('70%')
}
.padding(24)

} } 4.2 自定义布局组件 typescript

// 卡片组件 @Component struct CustomCard { // 参数定义 title: string = ''; content: string = ''; @Prop backgroundColor: string = '#FFFFFF'; @Prop onCardClick: () => void = () => {};

build() { Column({ space: 12 }) { // 标题栏 Row() { Text(this.title) .fontSize(18) .fontWeight(FontWeight.Bold)

    Blank()
    
    Image($r('app.media.more'))
      .width(20)
      .height(20)
  }
  .width('100%')
  
  // 内容区域
  Text(this.content)
    .fontSize(14)
    .fontColor('#666666')
    .lineHeight(20)
    .maxLines(3)
    .textOverflow({overflow: TextOverflow.Ellipsis})
  
  // 底部操作栏
  Row({ space: 10 }) {
    Button('查看详情')
      .width(100)
      .height(36)
      .fontSize(14)
    
    Blank()
    
    Button('分享')
      .width(80)
      .height(36)
      .fontSize(14)
      .backgroundColor('#F5F5F5')
      .fontColor('#333333')
  }
  .width('100%')
  .margin({ top: 16 })
}
.width('100%')
.padding(20)
.backgroundColor(this.backgroundColor)
.borderRadius(12)
.shadow({ radius: 8, color: '#10000000' })
.onClick(() => {
  this.onCardClick();
})

} }

// 使用自定义组件 @Entry @Component struct MainPage { build() { Column({ space: 20 }) { CustomCard({ title: '鸿蒙开发指南', content: '全面学习鸿蒙应用开发,从基础到实战...', backgroundColor: '#FFFFFF', onCardClick: () => { console.log('卡片被点击'); } })

  CustomCard({
    title: 'ArkTS进阶',
    content: '深入学习ArkTS语言特性和高级用法...',
    backgroundColor: '#F9F9F9'
  })
}
.padding(16)

} } 4.3 性能优化技巧 typescript

复制

下载

@Component struct OptimizedList { @State dataList: Array = [];

build() { List() { // 使用LazyForEach替代ForEach处理大数据量 LazyForEach(new DataSource(this.dataList), (item: DataItem) => { ListItem() { this.buildListItem(item); } }, (item: DataItem) => item.id.toString() ) } }

// 使用@Builder优化渲染性能 @Builder buildListItem(item: DataItem) { Row({ space: 12 }) { // 使用固定尺寸避免重新计算 Image(item.avatar) .width(44) .height(44) .objectFit(ImageFit.Cover)

  Column({ space: 4 }) {
    Text(item.title)
      .fontSize(16)
      .fontWeight(FontWeight.Medium)
      .layoutWeight(1)  // 使用layoutWeight避免多次测量
    
    Text(item.subtitle)
      .fontSize(14)
      .fontColor('#666666')
      .maxLines(1)
  }
  .alignItems(HorizontalAlign.Start)
}
.padding(12)

} } 五、实战:构建电商商品列表页 typescript

@Entry @Component struct ECommercePage { @State productList: Array = [ { id: 1, name: '华为Mate 60', price: 6999, image: 'mate60.jpg', stock: 50 }, { id: 2, name: '华为Watch 4', price: 2699, image: 'watch4.jpg', stock: 100 }, // ... 更多商品 ];

@State selectedCategory: string = 'all';

build() { Column() { // 1. 顶部导航栏 this.buildHeader();

  // 2. 分类标签
  this.buildCategoryTabs();
  
  // 3. 商品网格
  this.buildProductGrid();
  
  // 4. 底部导航
  this.buildFooter();
}
.width('100%')
.height('100%')
.backgroundColor('#F8F8F8')

}

// 顶部导航栏 @Builder buildHeader() { Row({ space: 12 }) { // 搜索框 TextInput({ placeholder: '搜索商品...' }) .width('70%') .height(40) .backgroundColor(Color.White) .borderRadius(20) .padding({ left: 16, right: 16 })

  // 购物车图标
  Image($r('app.media.cart'))
    .width(24)
    .height(24)
    .onClick(() => {
      // 跳转到购物车
    })
  
  // 消息图标
  Image($r('app.media.message'))
    .width(24)
    .height(24)
    .onClick(() => {
      // 跳转到消息
    })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor(Color.White)

}

// 分类标签 @Builder buildCategoryTabs() { Scroll(.horizontal) { Row({ space: 20 }) { ForEach(['全部', '手机', '平板', '手表', '笔记本'], (category: string) => { Text(category) .fontSize(16) .fontColor(this.selectedCategory === category ? '#007DFF' : '#333333') .fontWeight(this.selectedCategory === category ? FontWeight.Bold : FontWeight.Normal) .padding({ left: 20, right: 20, top: 8, bottom: 8 }) .backgroundColor(this.selectedCategory === category ? '#E6F2FF' : Color.Transparent) .borderRadius(20) .onClick(() => { this.selectedCategory = category; this.filterProducts(); }) } ) } .padding({ left: 16, right: 16, top: 12, bottom: 12 }) } .width('100%') .scrollBar(BarState.Off) }

// 商品网格 @Builder buildProductGrid() { Grid() { ForEach(this.productList, (product: Product) => { GridItem() { Column({ space: 10 }) { // 商品图片 Image(r(app.media.r(`app.media.{product.image}`)) .width('100%') .height(150) .objectFit(ImageFit.Cover) .borderRadius(8)

        // 商品信息
        Column({ space: 4 }) {
          Text(product.name)
            .fontSize(14)
            .fontColor('#333333')
            .maxLines(2)
            .textOverflow({overflow: TextOverflow.Ellipsis})
          
          Text(`¥${product.price}`)
            .fontSize(16)
            .fontColor('#FF6B00')
            .fontWeight(FontWeight.Bold)
          
          Row() {
            Text(`库存: ${product.stock}`)
              .fontSize(12)
              .fontColor('#999999')
            
            Blank()
            
            Button('加入购物车')
              .width(80)
              .height(28)
              .fontSize(12)
              .backgroundColor('#007DFF')
              .fontColor(Color.White)
              .borderRadius(14)
              .onClick(() => {
                this.addToCart(product);
              })
          }
          .width('100%')
        }
        .alignItems(HorizontalAlign.Start)
      }
      .padding(10)
      .backgroundColor(Color.White)
      .borderRadius(12)
      .shadow({ radius: 4, color: '#10000000' })
    }
  })
}
.columnsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(16)
.padding(16)
.layoutWeight(1)  // 占用剩余空间

}

// 底部导航 @Builder buildFooter() { Row({ space: 0 }) { ForEach(['首页', '分类', '购物车', '我的'], (tab: string, index: number) => { Column({ space: 4 }) { Image(r(`app.media.tab_{index}`)) .width(24) .height(24)

        Text(tab)
          .fontSize(12)
          .fontColor('#666666')
      }
      .width('25%')
      .padding({ top: 8, bottom: 8 })
      .onClick(() => {
        this.switchTab(index);
      })
    }
  )
}
.width('100%')
.backgroundColor(Color.White)
.border({
  width: { top: 1 },
  color: '#F0F0F0'
})

}

// 业务方法 filterProducts() { // 过滤商品逻辑 }

addToCart(product: Product) { console.log(添加商品:${product.name}); }

switchTab(index: number) { console.log(切换到标签:${index}); } } 六、常见布局问题与解决方案 问题1:组件超出屏幕边界 typescript

// 错误做法 Column() { Text('很长的文本内容...'.repeat(100)) } .width(300) // 固定宽度可能溢出

// 正确做法 Column() { Text('很长的文本内容...'.repeat(100)) .maxLines(3) // 限制行数 .textOverflow({overflow: TextOverflow.Ellipsis}) } .width('100%') // 使用百分比宽度 .padding(16) // 添加内边距 问题2:布局性能问题 typescript

// 优化前:每次都会重新计算 Column() { ForEach(this.data, (item) => { ComplexComponent({ data: item }) }) }

// 优化后:使用键值优化 Column() { ForEach(this.data, (item) => { ComplexComponent({ data: item }) }, (item) => item.id.toString()) // 提供唯一key }

// 进一步优化:使用@Builder分离渲染逻辑 @Builder buildContent() { ForEach(this.data, (item) => { this.buildItem(item) }) } 问题3:横竖屏适配 typescript

@Entry @Component struct OrientationDemo { @State isPortrait: boolean = true;

aboutToAppear() { // 监听屏幕方向变化 display.on('orientationChange', (orientation) => { this.isPortrait = orientation === display.Orientation.PORTRAIT; }); }

build() { Flex({ direction: this.isPortrait ? FlexDirection.Column : FlexDirection.Row }) { // 根据方向调整布局 if (this.isPortrait) { this.buildPortraitLayout(); } else { this.buildLandscapeLayout(); } } } } 七、总结与下期预告 7.1 本文要点回顾 基础组件:Text、Button、TextInput、Image的深度用法

六大布局:Column、Row、Stack、Flex、Grid、List的适用场景

高级技巧:响应式布局、自定义组件、性能优化

实战演练:完整电商页面的构建

7.2 最佳实践总结 优先使用百分比而非固定像素值

合理使用Flex布局处理复杂对齐需求

及时封装自定义组件提高代码复用性

使用@Builder优化渲染性能

考虑横竖屏适配提升用户体验

7.3 下期预告:《鸿蒙开发之:状态管理与数据绑定》 下篇文章将深入讲解:

@State、@Prop、@Link、@Watch装饰器的区别与使用场景

父子组件、兄弟组件之间的数据通信

全局状态管理的实现方案

实战:构建一个数据驱动的购物车应用

动手挑战 任务1:构建个人资料卡片 创建一个包含头像、姓名、职位、技能标签和个人简介的卡片组件,要求:

使用Column和Row布局

头像圆形显示

技能标签使用弹性换行布局

个人简介支持展开/收起

任务2:实现瀑布流布局 使用Grid或Flex布局实现类似Pinterest的瀑布流效果:

每列宽度固定

图片高度自适应

支持无限滚动加载

任务3:优化布局性能 为一个包含100个复杂列表项的页面进行性能优化:

使用LazyForEach

实现虚拟滚动

优化图片加载策略

将你的代码分享到评论区,我会挑选优秀实现进行详细点评!

常见问题解答 Q:Column和Flex有什么区别? A:Column是垂直排列的线性布局,适合简单垂直排列场景。Flex是更强大的弹性盒子布局,支持主轴方向、换行、对齐方式等高级特性,适合复杂布局需求。

Q:Grid和List如何选择? A:Grid适合展示网格状内容(如图片墙、商品列表),List适合展示线性列表(如聊天记录、新闻列表)。Grid的每个格子大小相对固定,List的每个项高度可以不同。

Q:如何实现固定头部和底部的布局? A:使用Column配合layoutWeight:

typescript

Column() { Header() // 固定顶部 .height(60)

Content() // 中间内容区域 .layoutWeight(1) // 占用剩余空间

Footer() // 固定底部 .height(50) } Q:如何处理键盘弹出时的布局调整? A:使用软键盘弹出监听和滚动调整:

typescript

window.on('keyboardHeightChange', (height: number) => { if (height > 0) { // 键盘弹出,调整布局 this.paddingBottom = height; } else { // 键盘收起 this.paddingBottom = 0; } }); PS:现在HarmonyOS应用开发者认证正在做活动,初级和高级都可以免费学习及考试,赶快加入班级学习啦:【注意,考试只能从此唯一链接进入】 developer.huawei.com/consumer/cn…