前言
我們在之前的文章中已經熟練掌握了線性佈局的語法,也就是 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 的位置完全依賴於 centerBox。alignRules 是核心屬性,它接受 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 的高性能開發之路上,學會“把佈局拍扁”是我們邁向高級開發者的重要一步。