本文字数: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({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…