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

程序员一鸣
• 阅读 2

前言

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

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
UIWebView长按保存图片和识别图片二维码的实现方案(使用缓存)
0x00需求:长按识别UIWebView中的二维码,如下图长按识别二维码0x01方案1:给UIWebView增加一个长按手势,激活长按手势时获取当前UIWebView的截图,分析是否包含二维码。核心代码:略优点:流程简单,可以快速实现。不足:无法实现保存UIWebView中图片,如果当前We
Wesley13 Wesley13
3年前
android手势滑动——左右滑动效果实现
/手势监听@authorlifengfeng/publicclassMainActivityextendsActivityimplementsOnTouchListener,OnGestureListener{//创
御弟哥哥 御弟哥哥
4年前
完美解决Android RyclerView嵌套滑动事件冲突
在Android项目开发中,为了实现需求和兼并用户体验,相信很多人都碰到滑动事件冲突的问题。在Android系统中事件分发机制是一个很重要的组成部分,由于这事件分发机制不是本文重点,故不在此多述,如果有想详细了解的可以自己搜下,网上有很多相关资料详细描述了Android事件分发机制。一、问题场景由于RecyclerView自身的优点,使得它已经基本
Stella981 Stella981
3年前
CocosCreator之分层管理的ListView
前言进入公众号回复listview即可获得demo的git地址。1.之前写的一篇文章《Creator之ScrollView那些事》中提到了官方Demo中提供的ListViewCtl,只是实现了纵向滑动,没有实现横向滑动。并且建议官方可以把功能做全然后放入组件库中供开发者使用。2.然后有个牛逼大神说这个ListView不ok。要我对自己
Stella981 Stella981
3年前
Android的消息循环与Handler机制理解
一、概念1、事件驱动型什么是事件驱动?就是有事了才去处理,没事就躺着不动。假如把用户点击按钮,滑动页面等这些都看作事件,事件产生后程序就执行相应的处理方法,就是属于事件驱动型。2、消息循环把需要处理的事件表示成一个消息,并且把这个消息放入一个队列。消息循环就是一循环,for或者while都一样。从消息队列里面取出未处理的消息,然后调用该消息的
Stella981 Stella981
3年前
HarmonyOS应用开发项目实战
鸿蒙2.0已经发布了有段时间了,目前网上也有些小demo了,但是缺乏稍微大点的项目代码。我准备计划开发一个稍微正式点的项目,我写了个初略的项目需求清单,来体验鸿蒙应用开发。目前我已经着手实现了其中的一些重要功能,某些功能发现鸿蒙暂时不支持,但是还是先写上吧,后面慢慢摸索。我会陆续更新连载此贴,一步步从0基础讲解项目开发过程,然后巩固鸿蒙应用开发知识点。有错误
程序员一鸣 程序员一鸣
4小时前
鸿蒙开发:使用nestedScroll解决滑动冲突
在实际的开发中,远远要比前边的案例复杂,比如我之前封装的刷新库,滚动组件上面有刷新头,下面也有加载尾,还有更复杂的,列表吸顶操作等等,一个nestedScroll属性是远远不够的,往往还要和滑动监听,是否滚动到了顶部和尾部等相结合,才能实现我们实际的效果。
点击按住说话按钮事件有延迟
问题原因:该问题原因是由于系统的某些手势delaysTouchesBegan属性为YES,当按钮处在某些特定位置时触摸事件会先被这些系统的手势拦截,系统不响应才会继续分发,而按钮的UIControlEventTouchDown事件是需要立即响应的,所以会导
程序员一鸣 程序员一鸣
1天前
HarmonyOs开发:轮播图Banner组件封装与使用
轮播图在每个项目中都很常见,鸿蒙中在容器组件中也提供了Swiper组件,用于子组件滑动轮播显示,和前端的使用起来也是异曲同工,我们先看下基本的用法。
陈杨 陈杨
1天前
鸿蒙5开发宝藏案例分享---瀑布流优化实战分享
以下是根据鸿蒙官方瀑布流优化案例整理的非官方技术分享,结合开发实战经验重新解读,加入更多场景分析和代码示例:🌟鸿蒙瀑布流性能优化实战:告别卡顿的宝藏指南!大家好!最近在鸿蒙文档里挖到一个性能优化宝藏库,原来官方早就准备好了各种场景的最佳实践!今天重点分享