Foreword
this article is based on Api13
there is a requirement to realize the top pull-down refresh, and the top title bar will be refreshed with gestures in the pull-down state, and the top will be sucked in the pull-up state, that is, tabs need to be fixed under the top title bar. The basic effect can be seen in the following figure. The following figure is a Demo. The actual requirement is that the top title Bar has a gradual change display, but these are not the key points.
What problems should be solved first? The first is pull-down refresh and pull-up loading, the second is the tabs component for ceiling, and the third is the gesture conflict problem. If these three problems are solved, the effect can basically be realized.
How to achieve
in order to ensure that the pull-down refresh is refreshed from the top, we need to determine the current sliding position. We can listen to the onReachStart event of the Scroll component and mark the top position in this event.
.onReachStart(() => {
this.listPosition = RefreshPositionEnum.TOP
})
Then, similarly, we also need to mark the middle and bottom positions. We can use onScrollFrameBegin to monitor the middle position. There is a point to note here, because the bottom is a waterfall flow component, and the middle and bottom positions can be completely handed over to the waterfall flow component, that is, the middle and bottom positions of the waterfall flow component are monitored.
.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 };
})
After the pull-down and pull-up positions are determined, the title bar is capped. It can be seen that the title bar is on the background at the bottom. Here we can use the Stack component to wrap:
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)
//下拉刷新相关
.onReachStart(() => {
this.listPosition = RefreshPositionEnum.TOP
})
Column() {
Text("顶部标题栏")
}
.width("100%")
.height(50)
.backgroundColor(Color.Transparent)
.justifyContent(FlexAlign.Center)
}.alignContent(Alignment.TopStart)
the most important thing is to refresh the components. You can use your own package or a three-way Package. Here I use one of my own packages. Of course, you can also use it.
The address is as follows:
https://ohpm.openharmony.cn/#/cn/detail/@abner%2Frefresh
source code
all the source codes are as follows. For refreshing the library, if you can switch your own, you can directly replace RefreshLayout. Of course, you can directly use the good ones I provided.
import { RefreshController, RefreshLayout, RefreshPositionEnum, WaterFlowView } from '@abner/refresh'
/**
* AUTHOR:AbnerMing
* DATE:2025/5/14
* INTRODUCE:吸顶页面-瀑布流方式-固定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)
//下拉刷新相关
.onReachStart(() => {
this.listPosition = RefreshPositionEnum.TOP
})
Column() {
Text("顶部标题栏")
}
.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(() => {
//模拟耗时
this.controller.finishRefresh()
}, 3000)
},
onLoadMore: () => {
setTimeout(() => {
//模拟耗时
this.controller.finishLoadMore()
}, 3000)
}
})
}
}
/*
* Author:AbnerMing
* Describe:这里仅仅是测试,实际应以业务需求为主,可以是任意得组件视图
*/
@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:瀑布流页面
*/
@Component
struct StickyStaggeredView {
@State pageType: number = 0
controller: RefreshController = new RefreshController() //刷新控制器
@State arr1: number[] = [] //实际情况当以tab指示器对应得数据为主,这里仅仅是测试
@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 //回调位置
@Prop enableScrollInteraction: boolean = true; //拦截列表
// 计算FlowItem宽/高
getSize() {
let ret = Math.floor(Math.random() * this.maxSize)
return (ret > this.minSize ? ret : this.minSize)
}
// 设置FlowItem的宽/高数组
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("测试数据" + 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,
})
}
}
Related Summary
it is not difficult in itself, just handle the sliding position and gesture. Of course, there are also two points of attention. One is nestedScroll, which solves the gesture conflict. As mentioned in the previous article, the other is to intercept the sliding event of the waterfall flow component and prohibit its sliding in some states.
This article tags: HarmonyOS/ArkUI
Top comments (0)