系列文章《鸿蒙开发从入门到专家》第 3 篇(初级篇)
前言
前两篇我们搞定了环境搭建和 ArkTS 语法。现在该真正动手搭界面了。ArkUI 是鸿蒙的声明式 UI 框架,它跟 Android 的 XML + Imperative 写法、iOS 的 UIKit 完全不同——你描述的是"界面应该长什么样",而不是"一步步怎么画出来"。
这种范式的转变有个好处:代码即 UI,逻辑和视图天然绑定。但也有个陷阱:如果你带着 Android XML 的思维写 ArkUI,写出来的代码会又臭又长。本文从最常用的布局组件入手,结合一个完整的"新闻列表页"实战,帮你建立 ArkUI 的正确写法直觉。
一、核心布局组件:Column、Row 与 Flex
ArkUI 的布局体系跟 Flutter 很像,都是基于 Flex 思想的。最基础的三件套是 Column(垂直排列)、Row(水平排列)和 Flex(通用弹性布局)。
1.1 Column 与 Row 的基础用法
@Entry
@Component
struct LayoutDemo {
build() {
Column({ space: 12 }) { // space: 子组件之间的间距
Row({ space: 8 }) {
Text('标签 A').padding(8).backgroundColor('#E6F2FF').borderRadius(4)
Text('标签 B').padding(8).backgroundColor('#FFF2E6').borderRadius(4)
Text('标签 C').padding(8).backgroundColor('#E6FFE6').borderRadius(4)
}
Divider().strokeWidth(1).color('#EEEEEE')
Column({ space: 6 }) {
Text('标题文本').fontSize(18).fontWeight(FontWeight.Bold)
Text('这是一段描述内容,用于展示 Column 内部的垂直排列效果。')
.fontSize(14)
.fontColor('#666666')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start) // 左对齐
.width('100%')
}
.padding(16)
.width('100%')
.backgroundColor(Color.White)
}
}
关键属性速查:
1.2 对齐方式详解
Column() {
Text('居中').fontSize(16)
}
.justifyContent(FlexAlign.Center) // 垂直居中
.alignItems(HorizontalAlign.Center) // 水平居中
.width('100%')
.height(200)
.backgroundColor('#F5F5F5')
justifyContent 控制主轴方向(Column 是垂直轴,Row 是水平轴),alignItems 控制交叉轴方向。这个跟 CSS Flexbox 完全一致,有前端基础的开发者可以直接迁移知识。
1.3 Flex:通用弹性布局
当 Column 和 Row 不够用的时候,直接用 Flex:
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) {
ForEach(this.tags, (tag: string) => {
Text(tag)
.padding({ top: 6, bottom: 6, left: 12, right: 12 })
.backgroundColor('#F0F0F0')
.borderRadius(16)
.margin(4)
})
}
.width('100%')
FlexWrap.Wrap 让子组件在空间不足时自动换行,非常适合标签云、图片网格这类场景。
二、列表与滑动容器:List、Swiper、Tabs
移动应用里列表是最常见的 UI 模式。ArkUI 提供了多种列表容器,选择合适的是性能优化的第一步。
2.1 List + LazyForEach:长列表的标准答案
class NewsItem {
id: string
title: string
summary: string
time: string
constructor(id: string, title: string, summary: string, time: string) {
this.id = id
this.title = title
this.summary = summary
this.time = time
}
}
@Entry
@Component
struct NewsListPage {
@State newsList: NewsItem[] = [
new NewsItem('1', 'HarmonyOS NEXT 5.0 正式发布', '纯血鸿蒙带来全新开发体验...', '2小时前'),
new NewsItem('2', 'ArkTS API 26 类型推断增强详解', 'as const 和类型守卫让代码更安全...', '5小时前'),
// ... 更多数据
]
build() {
List({ space: 1 }) {
LazyForEach(this.newsList, (item: NewsItem) => {
ListItem() {
Column() {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.summary)
.fontSize(14)
.fontColor('#999999')
.margin({ top: 6 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.time)
.fontSize(12)
.fontColor('#CCCCCC')
.margin({ top: 8 })
}
.padding(16)
.backgroundColor(Color.White)
.width('100%')
}
}, (item: NewsItem) => item.id) // key 生成器,必须唯一
}
.divider({ strokeWidth: 1, color: '#F0F0F0' })
.backgroundColor('#F5F5F5')
.width('100%')
.height('100%')
}
}
为什么要用 LazyForEach 而不是 ForEach?
黄金法则:列表数据超过一屏,一律用 LazyForEach,并且必须提供第三个参数(key 生成器)。
2.2 Swiper:轮播图实现
@Entry
@Component
struct BannerPage {
@State banners: string[] = [
'https://example.com/banner1.jpg',
'https://example.com/banner2.jpg',
'https://example.com/banner3.jpg'
]
build() {
Swiper() {
ForEach(this.banners, (url: string) => {
Image(url)
.width('100%')
.aspectRatio(2.5)
.objectFit(ImageFit.Cover)
.borderRadius(12)
})
}
.autoPlay(true)
.interval(3000)
.indicator(true)
.loop(true)
.padding(16)
}
}
2.3 Tabs:底部导航与顶部标签页
@Entry
@Component
struct MainPage {
@State currentIndex: number = 0
private tabsController: TabsController = new TabsController()
@Builder
TabBarBuilder(title: string, index: number, icon: Resource) {
Column() {
Image(icon)
.width(24)
.height(24)
.fillColor(this.currentIndex === index ? '#317AF7' : '#999999')
Text(title)
.fontSize(12)
.fontColor(this.currentIndex === index ? '#317AF7' : '#999999')
.margin({ top: 4 })
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = index
this.tabsController.changeIndex(index)
})
}
build() {
Tabs({ barPosition: BarPosition.End, controller: this.tabsController }) {
TabContent() {
HomeTab() // 自定义首页组件
}
.tabBar(this.TabBarBuilder('首页', 0, $r('app.media.icon_home')))
TabContent() {
DiscoveryTab() // 自定义发现页组件
}
.tabBar(this.TabBarBuilder('发现', 1, $r('app.media.icon_discovery')))
TabContent() {
ProfileTab() // 自定义我的页组件
}
.tabBar(this.TabBarBuilder('我的', 2, $r('app.media.icon_profile')))
}
.width('100%')
.height('100%')
.onChange((index: number) => {
this.currentIndex = index
})
}
}
Tabs 组件配合自定义 TabBarBuilder,可以做出微信风格的底部导航栏。TabsController 用于代码控制标签切换,比如从某个页面跳回首页并切到指定 Tab。
三、实战:搭建一个完整的新闻阅读页
把上面学的组件组合起来,搭一个带顶部轮播 + 分类标签 + 新闻列表的完整页面:
interface NewsItem {
id: string
title: string
summary: string
category: string
imageUrl?: string
time: string
}
@Entry
@Component
struct NewsHomePage {
@State currentCategory: string = '推荐'
@State newsList: NewsItem[] = []
@State bannerList: string[] = []
private categories: string[] = ['推荐', '科技', '财经', '体育', '娱乐']
aboutToAppear() {
// 模拟数据加载
this.bannerList = [
'https://picsum.photos/400/160?random=1',
'https://picsum.photos/400/160?random=2',
'https://picsum.photos/400/160?random=3'
]
this.loadNews('推荐')
}
private loadNews(category: string) {
this.currentCategory = category
// 模拟根据分类加载不同数据
this.newsList = Array.from({ length: 15 }, (_, i) => ({
id: `${category}-${i}`,
title: `${category}新闻标题 ${i + 1}`,
summary: `这是一段关于${category}的新闻摘要内容,用于展示列表项的排版效果...`,
category: category,
time: `${i + 1}小时前`
}))
}
@Builder
CategoryTabBuilder(category: string) {
Text(category)
.fontSize(14)
.fontWeight(this.currentCategory === category ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentCategory === category ? '#317AF7' : '#666666')
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor(this.currentCategory === category ? '#E6F2FF' : Color.Transparent)
.borderRadius(16)
.onClick(() => this.loadNews(category))
}
build() {
Column() {
// 顶部标题栏
Row() {
Text('今日资讯')
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.justifyContent(FlexAlign.Start)
// 轮播图
Swiper() {
ForEach(this.bannerList, (url: string) => {
Image(url)
.width('100%')
.aspectRatio(2.5)
.objectFit(ImageFit.Cover)
.borderRadius(8)
})
}
.autoPlay(true)
.interval(4000)
.indicator(true)
.padding({ left: 16, right: 16 })
// 分类标签
Row({ space: 8 }) {
ForEach(this.categories, (category: string) => {
this.CategoryTabBuilder(category)
})
}
.width('100%')
.padding(12)
.scrollable(ScrollDirection.Horizontal)
// 新闻列表
List({ space: 8 }) {
LazyForEach(this.newsList, (item: NewsItem) => {
ListItem() {
Row({ space: 12 }) {
Column({ space: 6 }) {
Text(item.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text(item.summary)
.fontSize(13)
.fontColor('#999999')
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.layoutWeight(1)
Text(item.time)
.fontSize(11)
.fontColor('#CCCCCC')
}
.height(90)
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Image('https://picsum.photos/120/90?random=' + item.id)
.width(100)
.height(75)
.objectFit(ImageFit.Cover)
.borderRadius(6)
}
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.width('100%')
}
}, (item: NewsItem) => item.id)
}
.padding({ left: 12, right: 12 })
.layoutWeight(1)
.backgroundColor('#F5F5F5')
}
.width('100%')
.height('100%')
.backgroundColor(Color.White)
}
}
这个页面的结构层次:
顶部标题栏(固定高度)
轮播 Banner(自动播放)
横向滚动的分类标签(可点击切换)
新闻列表(可纵向滚动,占据剩余空间)
四、响应式布局:一套代码适配多设备
鸿蒙应用要同时跑在手机、平板、折叠屏上,响应式布局是必修课。
4.1 百分比与媒体查询
@Entry
@Component
struct ResponsivePage {
@State isWideScreen: boolean = false
build() {
Column() {
if (this.isWideScreen) {
// 宽屏:左右分栏
Row() {
SideMenu().width('30%')
MainContent().width('70%')
}
} else {
// 窄屏:上下堆叠
Column() {
TopBar()
MainContent().layoutWeight(1)
}
}
}
.width('100%')
.height('100%')
.onAreaChange((oldArea, newArea) => {
// 监听尺寸变化,切换布局
this.isWideScreen = newArea.width > 600
})
}
}
4.2 七种自适应布局能力
ArkUI 提供了 7 种自适应布局机制:
小结与下篇预告
本文覆盖了 ArkUI 日常开发中最常用的内容:
Column/Row/Flex 是布局三件套,Flex 的 wrap 属性做标签云很方便
LazyForEach 是长列表的唯一正确选择,key 生成器不能省
Tabs + Swiper 组合可以搭出标准的 App 首页框架
响应式布局用
onAreaChange监听尺寸,条件渲染切换布局
下一篇(第 4 篇)进入中级内容:页面路由与导航。我们会深入对比 Router 和 Navigation 两种方案,分析它们在参数传递、生命周期管理、转场动画上的差异,并给出选型建议。