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

前言

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

相关推荐
xiaolizi5674892 小时前
安卓远程安卓(通过frp与adb远程)完全免费
android·远程工作
阿杰100012 小时前
ADB(Android Debug Bridge)是 Android SDK 核心调试工具,通过电脑与 Android 设备(手机、平板、嵌入式设备等)建立通信,对设备进行控制、文件传输、命令等操作。
android·adb
梨落秋霜2 小时前
Python入门篇【文件处理】
android·java·python
我不是8神2 小时前
gin与gorm框架知识点总结
ios·iphone·gin
遥不可及zzz4 小时前
Android 接入UMP
android
奋斗的小青年!!5 小时前
Flutter浮动按钮在OpenHarmony平台的实践经验
flutter·harmonyos·鸿蒙
Georgewu6 小时前
【HarmonyOS应用开发】鸿蒙应用实现横竖屏切换的两种方式以及注意事项
harmonyos
Coder_Boy_6 小时前
基于SpringAI的在线考试系统设计总案-知识点管理模块详细设计
android·java·javascript
冬奇Lab7 小时前
【Kotlin系列03】控制流与函数:从if表达式到Lambda的进化之路
android·kotlin·编程语言
冬奇Lab7 小时前
稳定性性能系列之十二——Android渲染性能深度优化:SurfaceFlinger与GPU
android·性能优化·debug