鸿蒙开发之:ArkUI组件布局全解析
传统Android/iOS开发采用命令式方式:你需要告诉系统"如何做"(先创建View,再设置属性,最后添加到父容器)。而鸿蒙ArkUI采用声明式方式:你只需要描述界面"是什么样子",系统自动处理渲染和更新。typescript// 命令式UI(传统Android)// 声明式UI(鸿蒙ArkUI)typescript// 卡片组件@Component// 参数定义build() {// 标题栏R
本文字数: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 本文要点回顾
-
基础组件: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应用开发者认证正在做活动,初级和高级都可以免费学习及考试,赶快加入班级学习啦:【注意,考试只能从此唯一链接进入】
https://developer.huawei.com/consumer/cn/training/classDetail/33f85412dc974764831435dc1c03427c?type=1?ha_source=hmosclass&ha_sourceld=89000248
更多推荐


所有评论(0)