前言
本文基于Api13
來了一個(gè)需求,要實(shí)現(xiàn)頂部下拉刷新,并且頂部的標(biāo)題欄,下拉狀態(tài)下跟隨手勢(shì)刷新,上拉狀態(tài)下進(jìn)行吸頂,也就是tabs需要固定在頂部標(biāo)題欄的下面,基本的效果可以看下圖,下圖是一個(gè)Demo,實(shí)際的需求,頂部標(biāo)題欄帶有漸變顯示,不過這些不是重點(diǎn)。

首先要解決什么問題?第一個(gè)就是下拉刷新和上拉加載,第二個(gè)就是tabs組件進(jìn)行吸頂,第三個(gè)就是手勢(shì)沖突問題了,這三個(gè)問題解決了,那么效果基本上也就能實(shí)現(xiàn)了。
如何實(shí)現(xiàn)
為了保證下拉刷新是從頂部刷新,需要判斷當(dāng)前的滑動(dòng)位置,我們可以監(jiān)聽Scroll組件的onReachStart事件,在這個(gè)事件里進(jìn)行標(biāo)記頂部的位置。
.onReachStart(() => {
this.listPosition = RefreshPositionEnum.TOP
})
那么同樣,中間和底部的位置,我們也需要標(biāo)記,中間的位置我們可以使用onScrollFrameBegin來監(jiān)聽,這里有一個(gè)點(diǎn)需要注意,因?yàn)榈撞渴且粋€(gè)瀑布流組件,中間和底部的位置,完全都可以交給瀑布流組件,也就是說監(jiān)聽瀑布流組件的中間和底部位置。
.onReachEnd(() => {
this.refreshPosition = RefreshPositionEnum.BOTTOM
if (this.onRefreshPosition != undefined) {
this.onRefreshPosition(this.refreshPosition)
}
})
.onScrollFrameBegin((offset: number) => {
if ((this.refreshPosition == RefreshPositionEnum.TOP && offset <= 0) || (
this.refreshPosition == RefreshPositionEnum.BOTTOM && offset >= 0
)) {
return { offsetRemain: 0 }
}
this.refreshPosition = RefreshPositionEnum.CENTER //中間
if (this.onRefreshPosition != undefined) {
this.onRefreshPosition(this.refreshPosition)
}
return { offsetRemain: offset };
})
下拉和上拉的位置確定好之后,那么就是標(biāo)題欄吸頂操作了,可以看到標(biāo)題欄是在底部的背景之上的,這里我們可以使用Stack組件進(jìn)行包裹:
Stack() {
Scroll() {
Column() {
Text("頭View")
.fontColor(Color.White)
.width("100%")
.height(200)
.backgroundColor(Color.Red)
.textAlign(TextAlign.Center)
.margin({ top: -50 })
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
this.testLayout(0)
}.tabBar(this.tabBuilder(0, "Tab1", this))
TabContent() {
this.testLayout(1)
}.tabBar(this.tabBuilder(1, "Tab2", this))
}
.barHeight(50)
.vertical(false)
.height("100%")
.onChange((index: number) => {
this.currentIndex = index
})
}.width("100%")
}
.padding({ top: 50 })
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.nestedScroll(this.listNestedScroll)
//下拉刷新相關(guān)
.onReachStart(() => {
this.listPosition = RefreshPositionEnum.TOP
})
Column() {
Text("頂部標(biāo)題欄")
}
.width("100%")
.height(50)
.backgroundColor(Color.Transparent)
.justifyContent(FlexAlign.Center)
}.alignContent(Alignment.TopStart)
最重要的就是刷新組件了,大家可以使用自己封裝的或者三方的都可以,這里我使用的是我自己封裝的一個(gè),當(dāng)然了大家也可以進(jìn)行使用。
地址如下:
https://ohpm.openharmony.cn/#/cn/detail/@abner%2Frefresh
源碼
所有的源碼如下,針對(duì)刷新庫(kù),大家如果可以切換自己的,直接替換RefreshLayout即可,當(dāng)然,你可以直接使用我提供好的。
import { RefreshController, RefreshLayout, RefreshPositionEnum, WaterFlowView } from '@abner/refresh'
/**
* AUTHOR:AbnerMing
* DATE:2024/5/14
* INTRODUCE:吸頂頁(yè)面-瀑布流方式-固定ActionBar
* */
@Entry
@Component
struct StickTopWaterPage {
@State listPosition: RefreshPositionEnum = RefreshPositionEnum.BOTTOM
@State fontColor: string = '#182431'
@State selectedFontColor: string = '#007DFF'
@State currentIndex: number = 0
controller: RefreshController = new RefreshController() //刷新控制器
@State enableScrollInteraction: boolean = true
@State listNestedScroll?: NestedScrollOptions = {
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
}
@Builder
tabBuilder(index: number, name: string, _this: StickTopWaterPage) {
Column() {
Text(name)
.fontColor(this.currentIndex === index ? this.selectedFontColor : this.fontColor)
.fontSize(16)
.fontWeight(this.currentIndex === index ? 500 : 400)
.lineHeight(22)
.margin({ top: 17, bottom: 7 })
Divider()
.strokeWidth(2)
.color('#007DFF')
.opacity(this.currentIndex === index ? 1 : 0)
}.width('100%')
}
@Builder
childView() {
Stack() {
Scroll() {
Column() {
Text("頭View")
.fontColor(Color.White)
.width("100%")
.height(200)
.backgroundColor(Color.Red)
.textAlign(TextAlign.Center)
.margin({ top: -50 })
Tabs({ barPosition: BarPosition.Start }) {
TabContent() {
this.testLayout(0)
}.tabBar(this.tabBuilder(0, "Tab1", this))
TabContent() {
this.testLayout(1)
}.tabBar(this.tabBuilder(1, "Tab2", this))
}
.barHeight(50)
.vertical(false)
.height("100%")
.onChange((index: number) => {
this.currentIndex = index
})
}.width("100%")
}
.padding({ top: 50 })
.scrollBar(BarState.Off)
.width('100%')
.height('100%')
.nestedScroll(this.listNestedScroll)
//下拉刷新相關(guān)
.onReachStart(() => {
this.listPosition = RefreshPositionEnum.TOP
})
Column() {
Text("頂部標(biāo)題欄")
}
.width("100%")
.height(50)
.backgroundColor(Color.Transparent)
.justifyContent(FlexAlign.Center)
}.alignContent(Alignment.TopStart)
}
build() {
Column() {
RefreshLayout({
itemLayout: () => {
this.childView()
},
controller: this.controller,
refreshPosition: this.listPosition, //定位位置
isRefreshTopSticky: true, //是否頂部吸頂
isRefreshTopTitleSticky: true,
enableScrollInteraction: (interaction: boolean) => {
this.enableScrollInteraction = interaction
},
onStickyNestedScroll: (nestedScroll: NestedScrollOptions) => {
this.listNestedScroll = nestedScroll
},
onRefresh: () => {
setTimeout(() => {
//模擬耗時(shí)
this.controller.finishRefresh()
}, 3000)
},
onLoadMore: () => {
setTimeout(() => {
//模擬耗時(shí)
this.controller.finishLoadMore()
}, 3000)
}
})
}
}
/*
* Author:AbnerMing
* Describe:這里僅僅是測(cè)試,實(shí)際應(yīng)以業(yè)務(wù)需求為主,可以是任意得組件視圖
*/
@Builder
testLayout(type: number) {
StickyStaggeredView({
pageType: type,
nestedScroll: this.listNestedScroll,
enableScrollInteraction: this.enableScrollInteraction,
onRefreshPosition: (refreshPosition: RefreshPositionEnum) => {
if (refreshPosition != RefreshPositionEnum.TOP) {
this.listPosition = refreshPosition
}
}
})
}
}
/*
* Author:AbnerMing
* Describe:瀑布流頁(yè)面
*/
@Component
struct StickyStaggeredView {
@State pageType: number = 0
controller: RefreshController = new RefreshController() //刷新控制器
@State arr1: number[] = [] //實(shí)際情況當(dāng)以tab指示器對(duì)應(yīng)得數(shù)據(jù)為主,這里僅僅是測(cè)試
@State arr2: number[] = []
private itemHeightArray: number[] = []
@State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]
@State minSize: number = 80
@State maxSize: number = 180
@Prop nestedScroll: NestedScrollOptions = {
scrollForward: NestedScrollMode.SELF_FIRST,
scrollBackward: NestedScrollMode.PARENT_FIRST
}
onRefreshPosition?: (refreshPosition: RefreshPositionEnum) => void //回調(diào)位置
@Prop enableScrollInteraction: boolean = true; //攔截列表
// 計(jì)算FlowItem寬/高
getSize() {
let ret = Math.floor(Math.random() * this.maxSize)
return (ret > this.minSize ? ret : this.minSize)
}
// 設(shè)置FlowItem的寬/高數(shù)組
setItemSizeArray() {
for (let i = 0; i < 100; i++) {
this.itemHeightArray.push(this.getSize())
}
}
aboutToAppear() {
for (let i = 0; i < 30; i++) {
this.arr1.push(i)
}
for (let i = 0; i < 50; i++) {
this.arr2.push(i)
}
this.setItemSizeArray()
}
@Builder
itemLayout(_this: StickyStaggeredView, _: Object, index: number) {
Column() {
Text("測(cè)試數(shù)據(jù)" + index)
}.width("100%")
.height(this.itemHeightArray[index % 100])
.backgroundColor(this.colors[index % 5])
}
build() {
WaterFlowView({
items: this.pageType == 0 ? this.arr1 : this.arr2,
itemView: (item: Object, index: number) => {
this.itemLayout(this, item, index)
},
nestedScroll: this.nestedScroll,
onRefreshPosition: this.onRefreshPosition,
enableScrollInteraction: this.enableScrollInteraction,
})
}
}
相關(guān)總結(jié)
本身并不難,處理好滑動(dòng)位置和手勢(shì)即可,當(dāng)然了,里面也有兩個(gè)注意的點(diǎn),一個(gè)是解決手勢(shì)沖突的nestedScroll,這個(gè)之前的文章中講過,還有一個(gè)就是攔截瀑布流組件的滑動(dòng)事件,在某些狀態(tài)下禁止它的滑動(dòng)。
本文標(biāo)簽:HarmonyOS/ArkUI