本文字数: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('https://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<DataItem> = [];
  
  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<Product> = [
    { 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.${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 本文要点回顾

  1. 基础组件:Text、Button、TextInput、Image的深度用法

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

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

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

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应用开发者认证正在做活动,初级和高级都可以免费学习及考试,赶快加入班级学习啦:【注意,考试只能从此唯一链接进入】
https://developer.huawei.com/consumer/cn/training/classDetail/33f85412dc974764831435dc1c03427c?type=1?ha_source=hmosclass&ha_sourceld=89000248

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐