Administrator
发布于 2026-08-30 / 69 阅读
0
0

ArkUI 声明式 UI 实战:组件、布局与状态管理入门

系列文章《鸿蒙开发从入门到专家》第 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)
  }
}

关键属性速查:

属性

Column 作用

Row 作用

space

子组件垂直间距

子组件水平间距

justifyContent

垂直方向对齐

水平方向对齐

alignItems

水平方向对齐

垂直方向对齐

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?

特性

ForEach

LazyForEach

渲染时机

一次性全部渲染

按需渲染可视区域 + 缓冲区

适用场景

数据量 < 20 条

长列表、大数据量

内存占用

随数据量线性增长

基本恒定

滑动性能

数据多时会卡顿

流畅

黄金法则:列表数据超过一屏,一律用 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)
  }
}

这个页面的结构层次:

  1. 顶部标题栏(固定高度)

  2. 轮播 Banner(自动播放)

  3. 横向滚动的分类标签(可点击切换)

  4. 新闻列表(可纵向滚动,占据剩余空间)

四、响应式布局:一套代码适配多设备

鸿蒙应用要同时跑在手机、平板、折叠屏上,响应式布局是必修课。

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 日常开发中最常用的内容:

  1. Column/Row/Flex 是布局三件套,Flex 的 wrap 属性做标签云很方便

  2. LazyForEach 是长列表的唯一正确选择,key 生成器不能省

  3. Tabs + Swiper 组合可以搭出标准的 App 首页框架

  4. 响应式布局用 onAreaChange 监听尺寸,条件渲染切换布局

下一篇(第 4 篇)进入中级内容:页面路由与导航。我们会深入对比 Router 和 Navigation 两种方案,分析它们在参数传递、生命周期管理、转场动画上的差异,并给出选型建议。

参考阅读


评论