鸿蒙开发:实现一个标题栏吸顶

前言

本文基于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

相关推荐
小草帽学编程38 分钟前
鸿蒙Next开发真机调试签名申请流程
android·华为·harmonyos
陈奕昆41 分钟前
5.2 HarmonyOS NEXT应用性能诊断与优化:工具链、启动速度与功耗管理实战
华为·harmonyos
dog shit3 小时前
web第十次课后作业--Mybatis的增删改查
android·前端·mybatis
科技道人4 小时前
Android15 launcher3
android·launcher3·android15·hotseat
哼唧唧_5 小时前
React Native开发鸿蒙运动健康类应用的项目实践记录
react native·harmonyos·harmony os5·运动健康
CYRUS_STUDIO8 小时前
FART 脱壳某大厂 App + CodeItem 修复 dex + 反编译还原源码
android·安全·逆向
Shujie_L11 小时前
【Android基础回顾】四:ServiceManager
android
Think Spatial 空间思维11 小时前
【实施指南】Android客户端HTTPS双向认证实施指南
android·网络协议·https·ssl
louisgeek12 小时前
Git 使用 SSH 连接
android