前言
本文基于Api13
来了一个需求,要实现顶部下拉刷新,并且顶部的标题栏,下拉状态下跟随手势刷新,上拉状态下进行吸顶,也就是tabs需要固定在顶部标题栏的下面,基本的效果可以看下图,下图是一个Demo,实际的需求,顶部标题栏带有渐变显示,不过这些不是重点。

首先要解决什么问题?第一个就是下拉刷新和上拉加载,第二个就是tabs组件进行吸顶,第三个就是手势冲突问题了,这三个问题解决了,那么效果基本上也就能实现了。
如何实现
为了保证下拉刷新是从顶部刷新,需要判断当前的滑动位置,我们可以监听Scroll组件的onReachStart事件,在这个事件里进行标记顶部的位置。
TypeScript
.onReachStart(() => {
this.listPosition = RefreshPositionEnum.TOP
})
那么同样,中间和底部的位置,我们也需要标记,中间的位置我们可以使用onScrollFrameBegin来监听,这里有一个点需要注意,因为底部是一个瀑布流组件,中间和底部的位置,完全都可以交给瀑布流组件,也就是说监听瀑布流组件的中间和底部位置。
TypeScript
.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 };
})
下拉和上拉的位置确定好之后,那么就是标题栏吸顶操作了,可以看到标题栏是在底部的背景之上的,这里我们可以使用Stack组件进行包裹:
TypeScript
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)
最重要的就是刷新组件了,大家可以使用自己封装的或者三方的都可以,这里我使用的是我自己封装的一个,当然了大家也可以进行使用。
地址如下:
ohpm.openharmony.cn/#/cn/detail...
源码
所有的源码如下,针对刷新库,大家如果可以切换自己的,直接替换RefreshLayout即可,当然,你可以直接使用我提供好的。
TypeScript
import { RefreshController, RefreshLayout, RefreshPositionEnum, WaterFlowView } from '@abner/refresh'
/**
* AUTHOR:AbnerMing
* DATE:2024/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,
})
}
}
相关总结
本身并不难,处理好滑动位置和手势即可,当然了,里面也有两个注意的点,一个是解决手势冲突的nestedScroll,这个之前的文章中讲过,还有一个就是拦截瀑布流组件的滑动事件,在某些状态下禁止它的滑动。
本文标签:HarmonyOS/ArkUI