博客 / 詳情

返回

鴻蒙 HarmonyOS 6 | ArkUI (05):佈局進階 相對佈局與 Flex 彈性佈局

前言

我們在之前的文章中已經熟練掌握了線性佈局的語法,也就是 Row 和 Column。它們就像是搭建樂高積木最基礎的磚塊,直觀且好用。但在實際的業務開發中,我們往往會遇到一些讓線性佈局捉襟見肘的場景。想象一下,設計師給你一張複雜的卡片設計圖:左上角是頭像,頭像右邊是暱稱,暱稱下面是簽名,右上角有一個關注按鈕,關注按鈕下面還有一個時間戳,而整個背景可能還有一張半透明的圖片。

如果我們只用線性佈局去實現,結果往往是 Row 套 Column,Column 又套 Row,Stack 再包一層。這種無休止的 套娃 現象,不僅讓代碼的可讀性變得極差,後期維護像是在解謎,更致命的是它對性能的損耗。在 ArkUI 的渲染管線中,每一個容器組件都需要參與測量(Measure)和佈局(Layout)的計算過程,層級越深,遞歸計算的開銷就越大,掉幀往往就是這樣產生的。

在鴻蒙 HarmonyOS 6 中,為了解決這種複雜界面的性能瓶頸,我們有了更強大的武器:RelativeContainer 相對佈局和 Flex 彈性佈局。

一、 拒絕佈局嵌套:RelativeContainer 的錨點哲學

RelativeContainer,顧名思義,就是通過定義子元素之間的 相對位置關係 來進行排版的。

這就好比我們在佈置一面照片牆,我們不會説“把這張照片放在第二行第三列”,而是説“把這張照片放在 A 照片的右邊,且頂部和 A 照片對齊”。在這個容器裏,子元素不再受限於線性排列的束縛,它們是自由的,唯一的約束來自於我們設定的 錨點

這種佈局模式最大的價值在於 扁平化。無論界面多麼複雜,理論上我們都可以通過一個 RelativeContainer 包裹所有的子元素來完成,將原本可能深達五六層的嵌套結構直接拍扁成一層。這對於渲染性能的提升是立竿見影的。在 API 20 中,RelativeContainer 的能力得到了進一步增強,它允許我們基於父容器__container__或者兄弟組件的 ID 來進行定位。

讓我們來看一段代碼片段,感受一下它的語法邏輯。假設我們要實現一個簡單的佈局:一個方塊居中,另一個方塊在這個方塊的右下方。

RelativeContainer() {
  // 這裏的 id 是必須的,它是定位的座標
  Row().width(100).height(100)
    .backgroundColor(Color.Red)
    .alignRules({
      center: { anchor: '__container__', align: VerticalAlign.Center },
      middle: { anchor: '__container__', align: HorizontalAlign.Center }
    })
    .id('centerBox') // 身份證

  Row().width(50).height(50)
    .backgroundColor(Color.Blue)
    .alignRules({
      top: { anchor: 'centerBox', align: VerticalAlign.Bottom }, // 頂部對齊到 centerBox 的底部
      left: { anchor: 'centerBox', align: HorizontalAlign.End }  // 左邊對齊到 centerBox 的右邊
    })
    .id('cornerBox')
}
.width(300).height(300)
.border({ width: 1 })

在這段代碼中,我們沒有使用任何嵌套容器。cornerBox 的位置完全依賴於 centerBoxalignRules 是核心屬性,它接受 top、bottom、left、right、center、middle 等方向的配置。每一個方向都需要指定一個 anchor(錨點對象)和一個 align(對齊方式)。

這種描述性的佈局方式,雖然在初次編寫時代碼量可能會稍微多一點點,但它換來的是極其清爽的組件結構和極佳的渲染性能。特別是對於複雜的列表 Item 卡片,使用 RelativeContainer 幾乎是標準答案。

二、 Flex 佈局:處理不確定的流式內容

如果説 RelativeContainer 是精密的瑞士軍表,每一個零件的位置都嚴絲合縫,那麼 Flex 佈局就是一根強韌的橡皮筋,它擅長處理那些 不確定 的場景。雖然 Row 和 Column 本質上也是 Flex 佈局的特例,但在 ArkUI 中,獨立的 Flex 容器提供了一個線性佈局無法做到的殺手鐗功能:換行(Wrap)

在實際開發中,最經典的場景就是 標籤雲 或者 搜索歷史記錄。這些標籤的寬度是不固定的,數量也是動態的。如果我們用 Row,一旦內容超出屏幕寬度,多餘的標籤就會被無情截斷或者導致佈局溢出。而 Flex 容器允許我們設置 flexWrap: FlexWrap.Wrap,當一行放不下時,子元素會自動折行到下一行,這在多終端適配時尤為重要,因為我們永遠不知道用户的屏幕有多寬。

看看下面這個標籤雲的實現,它展示了 Flex 的靈活性:

Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
  Text('HarmonyOS').padding(10).backgroundColor('#F1F3F5').margin(5).borderRadius(16)
  Text('ArkUI').padding(10).backgroundColor('#F1F3F5').margin(5).borderRadius(16)
  Text('高性能').padding(10).backgroundColor('#F1F3F5').margin(5).borderRadius(16)
  Text('分佈式架構').padding(10).backgroundColor('#F1F3F5').margin(5).borderRadius(16)
  Text('元服務').padding(10).backgroundColor('#F1F3F5').margin(5).borderRadius(16)
}
.width('100%')
.padding(10)

這裏的 wrap 屬性就是靈魂所在。我們還可以通過 justifyContent 來控制主軸上的對齊方式(比如居左、居中、兩端對齊),通過 alignItems 來控制交叉軸的對齊。相比於手動計算寬度去換行,Flex 容器將這些複雜的幾何計算全部在底層高效完成了。

三、 實戰:構建一個高性能的音樂播放卡片

為了真正掌握這兩個工具,我們來構建一個貼近實戰的 音樂播放控制卡片。這個卡片包含了專輯封面、歌名、歌手、播放/暫停按鈕、以及底部的標籤。

如果是傳統的思路,我們可能會這樣思考:先來一個 Row 放封面和右邊的文字區域,右邊的文字區域是一個 Column 放歌名和歌手,然後在這個 Row 外面再包一個 Row 放右邊的播放按鈕......停!這已經開始嵌套了。讓我們用 RelativeContainer 的思維重構它:所有的元素都是平級的,封面是左邊的錨點,播放按鈕是右邊的錨點,文字在它們中間,標籤用 Flex 放到底部。

下面是完整的代碼實現。請注意觀察我是如何使用 __container__ 作為父容器錨點,以及如何讓文本組件根據封面圖進行相對定位的。這種 扁平化 的代碼結構,在 DevEco Studio 的組件樹視圖中看也是隻有一層的,非常賞心悦目。

import { promptAction } from '@kit.ArkUI';

@Entry
@Component
export struct AdvancedLayoutPage {
  build() {
    Column() {
      // 頁面標題
      Text('佈局進階實戰')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })

      // -----------------------------------------------------------
      // 實戰案例:高性能音樂播放卡片
      // 使用 RelativeContainer 實現 0 嵌套的複雜佈局
      // -----------------------------------------------------------
      RelativeContainer() {
        // 1. 專輯封面 (左側基準錨點)
        Image($r('app.media.startIcon'))
          .width(80)
          .height(80)
          .borderRadius(12)
          .objectFit(ImageFit.Cover)
          .alignRules({
            top: { anchor: '__container__', align: VerticalAlign.Top },
            left: { anchor: '__container__', align: HorizontalAlign.Start }
          })
          .id('albumCover') // 設置 ID 供其他組件定位參考

        // 2. 播放按鈕 (右側基準錨點)
        // 我們先確定兩頭的位置,中間的內容就好放了
        Image($r('app.media.startIcon')) // 模擬播放圖標,實際開發請換成播放 SVG
          .width(40)
          .height(40)
          .fillColor('#0A59F7') // 圖片填充色
          .alignRules({
            center: { anchor: 'albumCover', align: VerticalAlign.Center }, // 垂直方向和封面居中
            right: { anchor: '__container__', align: HorizontalAlign.End } // 靠右對齊
          })
          .id('playBtn')
          .onClick(() => {
            promptAction.showToast({ message: '播放/暫停' });
          })

        // 3. 歌名 (定位在封面右側,按鈕左側)
        Text('HarmonyOS 6 狂想曲')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .alignRules({
            top: { anchor: 'albumCover', align: VerticalAlign.Top }, // 與封面頂部對齊
            left: { anchor: 'albumCover', align: HorizontalAlign.End }, // 在封面右邊
            right: { anchor: 'playBtn', align: HorizontalAlign.Start }  // 在按鈕左邊
          })
          .padding({ left: 12, right: 12 })
          .id('songTitle')

        // 4. 歌手信息 (在歌名下方)
        Text('ArkUI 樂隊')
          .fontSize(14)
          .fontColor('#999999')
          .alignRules({
            top: { anchor: 'songTitle', align: VerticalAlign.Bottom }, // 在歌名下面
            left: { anchor: 'songTitle', align: HorizontalAlign.Start } // 左對齊歌名
          })
          .padding({ left: 12, top: 4 })
          .id('artistName')

        // 5. 裝飾性的標籤 (展示 Flex 的嵌入使用)
        // 雖然外層是 RelativeContainer,但內部的小局部依然可以使用 Flex
        // 這裏的 Flex 作為一個整體,相對於封面定位
        Flex({ wrap: FlexWrap.NoWrap, direction: FlexDirection.Row }) {
          Text('無損音質')
            .fontSize(10)
            .fontColor(Color.White)
            .backgroundColor('#FFB020')
            .padding({ left: 4, right: 4, top: 2, bottom: 2 })
            .borderRadius(4)
            .margin({ right: 6 })

          Text('獨家')
            .fontSize(10)
            .fontColor('#0A59F7')
            .backgroundColor('#E6F0FF')
            .padding({ left: 4, right: 4, top: 2, bottom: 2 })
            .borderRadius(4)
        }
        .alignRules({
          bottom: { anchor: 'albumCover', align: VerticalAlign.Bottom }, // 底部與封面底部對齊
          left: { anchor: 'albumCover', align: HorizontalAlign.End }     // 左邊接封面右邊
        })
        .padding({ left: 12 })
        .id('tags')

      }
      .width('100%')
      .height(110) // 卡片整體高度
      .backgroundColor(Color.White)
      .borderRadius(16)
      .padding(16)
      .shadow({ radius: 8, color: '#1A000000', offsetY: 4 })
      .margin({ bottom: 20 })

      // -----------------------------------------------------------
      // 第二部分:Flex 佈局展示不確定寬度的標籤雲
      // -----------------------------------------------------------
      Text('熱門搜索 (Flex Wrap)')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .width('100%')
        .margin({ bottom: 12 })

      Flex({
        wrap: FlexWrap.Wrap, // 核心:允許換行
        justifyContent: FlexAlign.Start
      }) {
        this.TagItem('相對佈局')
        this.TagItem('性能優化')
        this.TagItem('扁平化')
        this.TagItem('HarmonyOS 6')
        this.TagItem('ArkTS')
        this.TagItem('一次開發多端部署')
        this.TagItem('元服務')
      }
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F1F3F5')
    .padding(20)
  }

  // 封裝一個小組件,方便生成標籤
  @Builder
  TagItem(text: string) {
    Text(text)
      .fontSize(14)
      .fontColor('#333333')
      .backgroundColor(Color.White)
      .padding({ left: 12, right: 12, top: 8, bottom: 8 })
      .borderRadius(20)
      .margin({ right: 10, bottom: 10 })
      .border({ width: 1, color: '#E0E0E0' })
  }
}

總結

當我們從線性佈局的思維定式中跳出來,開始擁抱 RelativeContainer 時,你會發現整個 UI 的構建邏輯變得豁然開朗。不再有深不見底的縮進,不再有為了一個對齊而被迫增加的容器。

配合 Flex 佈局處理動態流式內容的靈活性,我們能夠以極低的性能開銷構建出極其複雜的交互界面。在 HarmonyOS 6 的高性能開發之路上,學會“把佈局拍扁”是我們邁向高級開發者的重要一步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.