鴻蒙開發(fā):實(shí)現(xiàn)一個(gè)標(biāo)題欄吸頂

前言

本文基于Api13

來了一個(gè)需求,要實(shí)現(xiàn)頂部下拉刷新,并且頂部的標(biāo)題欄,下拉狀態(tài)下跟隨手勢(shì)刷新,上拉狀態(tài)下進(jìn)行吸頂,也就是tabs需要固定在頂部標(biāo)題欄的下面,基本的效果可以看下圖,下圖是一個(gè)Demo,實(shí)際的需求,頂部標(biāo)題欄帶有漸變顯示,不過這些不是重點(diǎn)。

首先要解決什么問題?第一個(gè)就是下拉刷新和上拉加載,第二個(gè)就是tabs組件進(jìn)行吸頂,第三個(gè)就是手勢(shì)沖突問題了,這三個(gè)問題解決了,那么效果基本上也就能實(shí)現(xiàn)了。

如何實(shí)現(xiàn)

為了保證下拉刷新是從頂部刷新,需要判斷當(dāng)前的滑動(dòng)位置,我們可以監(jiān)聽Scroll組件的onReachStart事件,在這個(gè)事件里進(jìn)行標(biāo)記頂部的位置。

      .onReachStart(() => {
        this.listPosition = RefreshPositionEnum.TOP
      })

那么同樣,中間和底部的位置,我們也需要標(biāo)記,中間的位置我們可以使用onScrollFrameBegin來監(jiān)聽,這里有一個(gè)點(diǎn)需要注意,因?yàn)榈撞渴且粋€(gè)瀑布流組件,中間和底部的位置,完全都可以交給瀑布流組件,也就是說監(jiān)聽瀑布流組件的中間和底部位置。

.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 };
  })

下拉和上拉的位置確定好之后,那么就是標(biāo)題欄吸頂操作了,可以看到標(biāo)題欄是在底部的背景之上的,這里我們可以使用Stack組件進(jìn)行包裹:

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)
      //下拉刷新相關(guān)
      .onReachStart(() => {
        this.listPosition = RefreshPositionEnum.TOP
      })

      Column() {
        Text("頂部標(biāo)題欄")
      }
      .width("100%")
      .height(50)
      .backgroundColor(Color.Transparent)
      .justifyContent(FlexAlign.Center)

    }.alignContent(Alignment.TopStart)

最重要的就是刷新組件了,大家可以使用自己封裝的或者三方的都可以,這里我使用的是我自己封裝的一個(gè),當(dāng)然了大家也可以進(jìn)行使用。

地址如下:

https://ohpm.openharmony.cn/#/cn/detail/@abner%2Frefresh

源碼

所有的源碼如下,針對(duì)刷新庫(kù),大家如果可以切換自己的,直接替換RefreshLayout即可,當(dāng)然,你可以直接使用我提供好的。

import { RefreshController, RefreshLayout, RefreshPositionEnum, WaterFlowView } from '@abner/refresh'

/**
 * AUTHOR:AbnerMing
 * DATE:2024/5/14
 * INTRODUCE:吸頂頁(yè)面-瀑布流方式-固定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)
      //下拉刷新相關(guān)
      .onReachStart(() => {
        this.listPosition = RefreshPositionEnum.TOP
      })

      Column() {
        Text("頂部標(biāo)題欄")
      }
      .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(() => {
            //模擬耗時(shí)
            this.controller.finishRefresh()
          }, 3000)
        },
        onLoadMore: () => {
          setTimeout(() => {
            //模擬耗時(shí)
            this.controller.finishLoadMore()
          }, 3000)
        }
      })
    }

  }

  /*
  * Author:AbnerMing
  * Describe:這里僅僅是測(cè)試,實(shí)際應(yīng)以業(yè)務(wù)需求為主,可以是任意得組件視圖
  */
  @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:瀑布流頁(yè)面
*/
@Component
struct StickyStaggeredView {
  @State pageType: number = 0
  controller: RefreshController = new RefreshController() //刷新控制器
  @State arr1: number[] = [] //實(shí)際情況當(dāng)以tab指示器對(duì)應(yīng)得數(shù)據(jù)為主,這里僅僅是測(cè)試
  @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 //回調(diào)位置
  @Prop enableScrollInteraction: boolean = true; //攔截列表

  // 計(jì)算FlowItem寬/高
  getSize() {
    let ret = Math.floor(Math.random() * this.maxSize)
    return (ret > this.minSize ? ret : this.minSize)
  }

  // 設(shè)置FlowItem的寬/高數(shù)組
  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("測(cè)試數(shù)據(jù)" + 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,
    })
  }
}

相關(guān)總結(jié)

本身并不難,處理好滑動(dòng)位置和手勢(shì)即可,當(dāng)然了,里面也有兩個(gè)注意的點(diǎn),一個(gè)是解決手勢(shì)沖突的nestedScroll,這個(gè)之前的文章中講過,還有一個(gè)就是攔截瀑布流組件的滑動(dòng)事件,在某些狀態(tài)下禁止它的滑動(dòng)。

本文標(biāo)簽:HarmonyOS/ArkUI

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容