HarmonyOS鴻蒙吸頂效果的實現(xiàn)方案詳解
吸頂效果,即頁面滾動時讓某一區(qū)域(如分類導(dǎo)航欄)固定在頂部,隨頁面滾動"貼"在標題欄下方,類似 Android/iOS 原生設(shè)計中的 StickyHeader。 本文結(jié)合實際生產(chǎn)頁面 HomeExplorePage,深入解析 HarmonyOS ArkUI 中實現(xiàn) Tabs 吸頂?shù)耐暾桨浮?/p>
先看結(jié)論
- 吸頂是 Tabs + 父子滾動聯(lián)動 共同實現(xiàn)的,不是單一屬性生效。
- 關(guān)鍵配置只有四個:
height('100%')、Tabs.height(calc(...))、nestedScroll(...)、edgeEffect(None, { alwaysEnabled: true })。 - 其中兩個最容易遺漏:
.barHeight('auto'):讓 tabBar 高度跟隨內(nèi)容alwaysEnabled: true:邊界處持續(xù)感知,避免“卡一下”
閱讀導(dǎo)航
- 想快速落地:看「四個關(guān)鍵屬性」+「方案總結(jié)」
- 想理解原理:看「關(guān)鍵③ nestedScroll」和「關(guān)鍵④ edgeEffect」
- 想直接復(fù)制:看「完整代碼示例」
一、效果演示
| 交互 | 行為 |
|---|---|
| 頁面初始 | 頂部 Banner 頭圖 + 下方綠色 TabBar 均可見 |
| 手指下滑 | WaterFlow 先滾動 → 列表到頂后外層 Scroll 繼續(xù) → TabBar 被推出屏幕 |
| 手指上滑 | 外層 Scroll 先滾回 → TabBar 貼到標題欄下方 → 列表繼續(xù)滾動(吸頂完成) |
二、布局結(jié)構(gòu)
整體布局采用 外層 Scroll + 內(nèi)層 Tabs + 子列表 的嵌套結(jié)構(gòu),與業(yè)界通用方案一致:
NavDestination
└── Stack ← 根容器,提供 z 軸層級
├── Stack 頂部標題欄 ← zIndex=2,始終在最前
│ height = statusBar + 標題高度
│
└── Scroll 外層滾動器 ← 【關(guān)鍵①】height='100%'
└── Column ← 子內(nèi)容回推總高度 → 產(chǎn)生滾動空間
├── Banner Column ← 頭圖區(qū),上滑滾出屏幕
└── Tabs 吸頂區(qū)域 ← 【關(guān)鍵②】calc(100% - avoidance)
└── TabContent
└── WaterFlow/List ← 【關(guān)鍵③nestedScroll + 關(guān)鍵④edgeEffect】三、四個關(guān)鍵屬性(重點)
要實現(xiàn)流暢的吸頂效果,必須同時滿足以下四個條件,缺一不可。
關(guān)鍵① —— 外層滾動器:height='100%'
Scroll(this.outerScroller) {
Column() { /* 子內(nèi)容 */ }
.width('100%')
}
.width('100%')
.height('100%') // ← 關(guān)鍵:不約束子 Column 高度,由內(nèi)容回推總高度 → 產(chǎn)生滾動空間
.scrollBar(BarState.Off)
作用: height('100%') 表示 Scroll 的高度基準等于父容器,不對子內(nèi)容做高度截斷。子 Column 的總高度由內(nèi)部所有子組件回推得到,當內(nèi)容超出屏幕時自然產(chǎn)生滾動區(qū)域。
常見誤區(qū):
| 錯誤寫法 | 問題 |
|---|---|
內(nèi)層 Stack 設(shè)置 height('100%') | 鎖死高度,子 Column 無法撐開,Scroll 無滾動空間 |
constraintSize({ minHeight: '100%' }) | 同樣約束高度,效果等同于上例 |
Tabs 使用 layoutWeight(1) + height('100%') | 高度基準偏小,列表內(nèi)容填不滿或溢出 |
關(guān)鍵② —— Tabs 高度與 barHeight
// 避讓高度 = 狀態(tài)欄高度 + 標題欄高度
private getAvoidanceHeight(): number {
return WindowHelper.statusBarHeight + 25
}
Tabs({ index: $$this.selectedTabIndex }) {
TabContent() { this.tabContentBuilder() }
.tabBar(this.tabBarBuilder())
}
// 【關(guān)鍵②-a】Tabs 高度固定為 Scroll.Column 的剩余空間
.height(`calc(100% - ${this.getAvoidanceHeight()}vp)`)
// 【關(guān)鍵②-b】tabBar 高度由內(nèi)容撐開
.barHeight('auto')
.scrollable(false) // 禁用 Tabs 內(nèi)置滾動,由子列表承載
.barPosition(BarPosition.Start) // tabBar 在內(nèi)容上方(縱向吸頂)
.clip(true)
關(guān)鍵②-a:height = calc(100% - avoidanceHeight)
原理:
- 外層 Scroll.Column 布局:頂部 Banner(180) + hint 提示 + Tabs
- Scroll 設(shè)定
height='100%'(關(guān)鍵①),不約束自身高度,由子元素回推總高 - 此時 Column 總高度 = Banner(180) + hint + Tabs
- 如果 Tabs 不設(shè)高度,會被內(nèi)容撐開,可能把 Tabs 自身的一部分內(nèi)容頂出屏幕底部
使用 calc(100% - avoidanceHeight) 后:
| 高度計算 | 值 |
|---|---|
| Scroll 總高 | 屏幕高度(100%) |
| 減去標題欄 | avoidanceHeight = statusBarHeight + 25 |
| Tabs 實際高度 | calc(100% - avoidanceHeight) = 屏幕高度 - 標題欄高度 |
效果: Tabs 精確填滿 Scroll.Column 減去上方 Banner 和 hint 后的剩余垂直空間,列表內(nèi)容不會溢出屏幕。
關(guān)鍵②-b:barHeight('auto')
作用: tabBar 的高度由 Builder 內(nèi)容自動撐開,而非固定數(shù)值。
在 tabBarBuilder 中,Row 高度為 44(tabs 文字行)+ Divider(1),如果使用固定數(shù)值如 barHeight(48):
- tabBar 內(nèi)容變化(如增加一行文字)時,高度不會自適應(yīng)
- 固定數(shù)值與 Builder 實際高度不匹配時,會產(chǎn)生裁剪或留白
barHeight('auto') 讓 HarmonyOS 根據(jù) tabBarBuilder() 實際渲染出的高度來計算 tabBar 區(qū)域,保證與 Builder 內(nèi)容精確匹配。
附加配置:
scrollable(false):禁用 Tabs 內(nèi)置滑動切換,內(nèi)容滾動完全由 WaterFlow 承載,避免兩層滾動互相干擾barPosition(BarPosition.Start):tabBar 位于內(nèi)容上方(縱向),橫向 Tabs 吸頂用Endclip(true):裁剪超出區(qū)域,防止 Tabs 內(nèi)容越界
關(guān)鍵③ —— nestedScroll:父子滾動聯(lián)動
WaterFlow({
scroller: this.waterFlowScroller,
footer: this.footerBuilder()
}) {
LazyForEach(this.dataSource, (item: number) => {
FlowItem() { this.waterFlowItem(item) }
}, (item: number) => item.toString())
}
// 【關(guān)鍵③】nestedScroll:協(xié)調(diào)父子滾動容器之間的優(yōu)先級
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST, // 向下滾動:列表先滾,到頂后外層 Scroll 接管
scrollBackward: NestedScrollMode.SELF_FIRST // 向上滾動:列表先滾回,再由外層 Scroll 接手
})
核心作用: 決定父子兩個滾動容器“誰先響應(yīng)滾動”。
scrollForward: PARENT_FIRST(向下滾動 / 手指上滑):
手指向上滑 → WaterFlow 響應(yīng)滾動
↓
WaterFlow 內(nèi)容滾動,列表向上移動
↓
WaterFlow 到達列表頂部(內(nèi)容已無剩余)
↓
滾動權(quán)交給外層 Scroll
↓
外層 Scroll 繼續(xù)向上滾動,Banner 和 TabBar 被推出可視區(qū)
↓
TabBar "吸"在屏幕頂部(已滾出外層 Scroll 的可視區(qū),緊貼標題欄)scrollBackward: SELF_FIRST(向上滾動 / 手指下滑):
手指向下滑 → WaterFlow 優(yōu)先響應(yīng)
↓
WaterFlow 到達列表底部(內(nèi)容已無剩余)
↓
WaterFlow 響應(yīng)手指繼續(xù)滾動(上拉手勢)
↓
WaterFlow 到頂,滾動權(quán)交給外層 Scroll
↓
外層 Scroll 向下滑動,TabBar 從標題欄下方落回
↓
TabBar 重新出現(xiàn)在屏幕中
| 模式 | 值 | 行為 |
|---|---|---|
| scrollForward(下滑) | PARENT_FIRST | 列表先滾,到頂后交給外層繼續(xù) |
| scrollBackward(上滑) | SELF_FIRST | 外層先滾,TabBar 落位后列表再滾 |
如果子列表是
List,配置方式完全相同,將WaterFlow替換為List即可。
關(guān)鍵④ —— edgeEffect:吸頂邊界處理
WaterFlow({ ... })
.nestedScroll({ ... })
// 【關(guān)鍵④】edgeEffect:禁用回彈 + 保持邊緣感知,確保吸頂絲滑
.edgeEffect(EdgeEffect.None, { alwaysEnabled: true })
參數(shù)詳解:
EdgeEffect.None — 禁用彈性回彈
| 回彈效果 | 表現(xiàn) | 吸頂場景下的影響 |
|---|---|---|
Spring(默認) | 列表到頂/到底時,手指松開后內(nèi)容彈回 | TabBar 在屏幕頂部抖動,無法穩(wěn)定"吸住" |
None | 列表到頂/到底時,無回彈動畫 | 配合 nestedScroll,TabBar 平滑停在標題欄下方 |
alwaysEnabled: true — 保持邊緣滾動感知(關(guān)鍵)
這是最容易遺漏的參數(shù),作用如下:
沒有 alwaysEnabled=true: WaterFlow 到頂 → 外層 Scroll 無法感知"已到頂" → 卡頓,無法交接滾動權(quán) 有 alwaysEnabled=true: WaterFlow 到頂 → 仍能感知邊緣狀態(tài) → nestedScroll 正常觸發(fā) → 絲滑交接
常見誤區(qū): 如果只寫 .edgeEffect(EdgeEffect.None) 而不設(shè)置 alwaysEnabled: true,會出現(xiàn)"卡頓"現(xiàn)象——列表到頂后外層 Scroll 短暫不響應(yīng),然后突然搶走滾動權(quán),導(dǎo)致 TabBar 在頂部抖動。
綜合效果:
- 子列表到頂時,不觸發(fā)回彈動畫
- 吸頂邊界時,
alwaysEnabled: true保證滾動感知不斷聯(lián) - 配合
nestedScroll實現(xiàn)"列表到頂 → TabBar 絲滑吸住"的體驗
四、完整代碼示例
以下為抽取核心邏輯后的最小可運行示例,基于 Demo 頁面 WaterFlowStickyDemoPage:
import { WindowHelper } from '@zebra/foundation/src/main/ets/utils/WindowHelper'
@ComponentV2
export struct WaterFlowStickyDemoPage {
// ── 滾動器 ──────────────────────────────────────────────────
private outerScroller: Scroller = new Scroller() // 【關(guān)鍵①】外層滾動
private waterFlowScroller: Scroller = new Scroller() // 【關(guān)鍵③】子列表滾動
// ── 避讓高度 ──────────────────────────────────────────────
// = 狀態(tài)欄高度 + 標題欄高度
private getAvoidanceHeight(): number {
return WindowHelper.statusBarHeight + 25
}
// ── TabBar ──────────────────────────────────────────────────
@Builder
tabBarBuilder() {
Column() {
Row() {
ForEach(['推薦', '熱門', '最新'], (tab: string, index: number) => {
Column() {
Text(tab)
.fontSize(15)
.fontColor(this.selectedTabIndex === index ? '#00CC66' : '#999999')
Divider()
.width(this.selectedTabIndex === index ? 20 : 0)
.height(2)
.backgroundColor('#00CC66')
.margin({ top: 4 })
}
.width(60)
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.height(44)
Divider().width('100%').strokeWidth(1).color('#C8E6C9')
}
.width('100%')
.backgroundColor('#E8F5E9')
}
// ── TabContent ─────────────────────────────────────────────
@Builder
tabContentBuilder() {
WaterFlow({
scroller: this.waterFlowScroller
}) {
LazyForEach(this.dataSource, (item: number) => {
FlowItem() {
Column() {
Text(`${item}`)
}
.width('100%')
.height(88 + (item % 7) * 28)
.backgroundColor(colors[item % colors.length])
.borderRadius(8)
}
}, (item: number) => item.toString())
}
.columnsTemplate('1fr 1fr')
.columnsGap(12)
.rowsGap(12)
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.scrollBar(BarState.Off)
// 【關(guān)鍵③】nestedScroll
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
// 【關(guān)鍵④】edgeEffect
.edgeEffect(EdgeEffect.None, { alwaysEnabled: true })
}
// ── Tabs 主體 ───────────────────────────────────────────────
@Builder
contentBuilder() {
Tabs({ index: $$this.selectedTabIndex }) {
TabContent() { this.tabContentBuilder() }
.tabBar(this.tabBarBuilder())
}
// 【關(guān)鍵②-a】Tabs 高度 = Scroll.Column 剩余空間
.height(`calc(100% - ${this.getAvoidanceHeight()}vp)`)
// 【關(guān)鍵②-b】tabBar 高度由 Builder 內(nèi)容撐開
.barHeight('auto')
.scrollable(false)
.barPosition(BarPosition.Start)
.clip(true)
}
// ── build ───────────────────────────────────────────────────
build() {
NavDestination() {
Stack({ alignContent: Alignment.Top }) {
// 頂部標題欄
Column() {
Text('頁面標題')
.fontSize(17)
.height(25)
}
.width('100%')
.height(WindowHelper.statusBarHeight + 25)
.padding({ left: 16 })
.zIndex(2)
// 外層 Scroll 【關(guān)鍵①】
Scroll(this.outerScroller) {
Column() {
// Banner 頭圖區(qū)
Column() {
Text('Banner Area')
}
.width('100%')
.height(180)
.backgroundColor('#E8F5E9')
// Tabs 吸頂區(qū)域 【關(guān)鍵②③④】
this.contentBuilder()
}
.width('100%')
}
.width('100%')
.height('100%') // ← 關(guān)鍵①:height='100%',內(nèi)容回推高度
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
}
.hideTitleBar(true)
}
}
五、實戰(zhàn):HomeExplorePage 中的吸頂實現(xiàn)
生產(chǎn)環(huán)境中的 HomeExplorePage 與 Demo 邏輯完全一致,核心配置如下:
// HomeExplorePage.ets
// ① 外層滾動器 height='100%'
Scroll(this.exploreScroller) {
Column() {
// Banner + Tabs
Tabs()
// 【關(guān)鍵②-a】Tabs 高度 = Scroll.Column 剩余空間
.height(`calc(100% - ${this.explorePageDataModel.avoidanceHeight}vp)`)
// 【關(guān)鍵②-b】tabBar 高度由 Builder 內(nèi)容撐開
.barHeight('auto')
.scrollable(false)
.barPosition(BarPosition.Start)
.clip(true)
}
}
.width('100%')
.height('100%') // ← 關(guān)鍵①
// ② WaterFlow nestedScroll + edgeEffect
WaterFlow({ scroller: this.waterFlowScroller }) {
LazyForEach(...)
}
// 【關(guān)鍵③】nestedScroll
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
// 【關(guān)鍵④】edgeEffect
.edgeEffect(EdgeEffect.None, { alwaysEnabled: true })
六、方案總結(jié)
| 關(guān)鍵屬性 | 配置 | 作用 |
|---|---|---|
| ① 外層 Scroll | height('100%') | 由子內(nèi)容回推總高度,產(chǎn)生滾動區(qū)域 |
| ②-a Tabs 高度 | calc(100% - avoidanceHeight) | 固定占據(jù) Scroll.Column 剩余空間,基準與 Scroll 一致 |
| ②-b barHeight | barHeight('auto') | tabBar 高度由 Builder 內(nèi)容撐開,避免固定數(shù)值裁剪 |
| ③ nestedScroll | PARENT_FIRST + SELF_FIRST | 列表與外層滾動器平滑聯(lián)動,實現(xiàn) TabBar 吸頂 |
| ④ edgeEffect | EdgeEffect.None, { alwaysEnabled: true } | 吸頂邊界時不觸發(fā)回彈,保持 TabBar 穩(wěn)定 |
核心心法: 吸頂?shù)谋举|(zhì)是 父子滾動器的嵌套聯(lián)動 — 子列表到頂后讓出滾動權(quán)給外層 Scroll,上滑時外層 Scroll 先滾回再交還滾動權(quán)給列表。四個關(guān)鍵屬性缺一,聯(lián)動鏈條即斷裂。
到此這篇關(guān)于HarmonyOS鴻蒙吸頂效果實現(xiàn)方案的文章就介紹到這了,更多相關(guān)HarmonyOS鴻蒙吸頂效果內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
eBay 打造基于 Apache Druid 的大數(shù)據(jù)實時監(jiān)控系統(tǒng)
Apache Druid 是一個用于大數(shù)據(jù)實時查詢和分析的高容錯、高性能開源分布式時序數(shù)據(jù)庫系統(tǒng),旨在快速處理大規(guī)模的數(shù)據(jù),并能夠?qū)崿F(xiàn)快速查詢和分析。這篇文章主要介紹了eBay 如何打造基于 Apache Druid 的大數(shù)據(jù)實時監(jiān)控系統(tǒng)?需要的朋友可以參考下2019-06-06
Prometheus和NodeExporter安裝監(jiān)控數(shù)據(jù)說明
這篇文章主要為大家介紹了Prometheus和node?exporter安裝監(jiān)控數(shù)據(jù)說明,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07

