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