ArkUI实战,自定义下拉刷新组件RefreshList

Posted llew2011

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArkUI实战,自定义下拉刷新组件RefreshList相关的知识,希望对你有一定的参考价值。

下拉刷新是一个高频使用的功能,ArkUI 开发框架也提供了下拉刷新组件 Refresh,该组件的使用非常简单,读者可参阅笔者在《ArkUI实战》第六章 第 5 小节 的介绍,本文笔者讲解一下笔者在项目上实现的一个下拉刷新组件 RefreshList,该组件的运行效果如下图所示:

  • 布局拆分
    下拉刷新组件都是分为上下两部分,上边是刷新头:refreshHead,该刷新头根据手指的下滑距离提示是否达到刷新条件;下边是刷新体:refreshContent,当触发下拉刷新条件后对外回调,从而实现内容更新。笔者实现的 RefreshList 也是按照以上布局实现的,简化图如下所示:

    默认情况下 refreshHead 是布局在 RefreshList 可视区域外边,笔者在第三章 第 1 小节 讲解过可以使用 position() 方法实现布局定位,简化代码如下所示:
@Component struct RefreshList 
  build() 
    Column() 
      Row() 
        // header布局
      
      .id("refresh_header")
      .width("100%")
      .height(50)
      .position( // 利用该属性,把refresh_header布局在 Column 顶部
        x: 0,
        y: -50
      )

      Column() 
        // content 布局
      
      .id("refresh_content")
      .width("100%")
      .height("100%")
      .position( // 利用该属性,把refresh_content布局向上做偏移
        x: 0,
        y: 0
      )
    
    .id("refresh_list")
    .width("100%")
    .height("100%")
  

  • 滑动处理
    ArkUI 开发框架对于手势事件的处理遵循 W3C 标准,首先是目标捕获阶段,然后再是事件冒泡阶段,下拉刷新的操作就是在事件冒泡阶段处理的,因此直接实现 refresh_list 的 onTouch() 方法即可,在该方法内根据手指的滑动距离动态实现 refreshHeader 和 refreshContent 的布局定位即可,简化代码如下所示:
@Component struct RefreshList 

  private refreshHeaderHeight: number = 50;
  private offsetY: number = -this.refreshHeaderHeight;
  private lastX: number;
  private lastY: number;
  private downY: number;

  build() 
    Column() 
      Row()
      .id("refresh_header")
      .width("100%")
      .height(this.refreshHeaderHeight)
      .backgroundColor("#bbaacc")
      .position(
        x: 0,
        y: this.offsetY
      )

      Column() 
      
      .id("refresh_content")
      .width("100%")
      .height("100%")
      .backgroundColor("#aabbcc")
      .position(
        x: 0,
        y: this.offsetY + this.refreshHeaderHeight
      )
    
    .id("refresh_list")
    .width("100%")
    .height("100%")
    .onTouch((event) => 
      if (event.type == TouchType.Down) 
        // 处理 down 事件
        this.onTouchDown(event);
       else if (event.type == TouchType.Move) 
        // 处理 move 事件
        this.onTouchMove(event);
       else if (event.type == TouchType.Cancel || event.type == TouchType.Up) 
        // 处理 up 事件
        this.onTouchUp(event);
      
    )
  

  private onTouchDown(event: TouchEvent) 
    this.lastX = event.touches[0].screenX;
    this.lastY = event.touches[0].screenY;
    this.downY = this.lastY;
  

  private onTouchMove(event: TouchEvent) 
    let currentX = event.touches[0].screenX;
    let currentY = event.touches[0].screenY;
    let deltaX = currentX - this.lastX;
    let deltaY = currentY - this.lastY;

    if (Math.abs(deltaX) < Math.abs(deltaY) && Math.abs(deltaY) > 5) 
      // 达到滑动条件
    
  

  private onTouchUp(event: TouchEvent) 
  

  • 滑动冲突
    由于 refreshContent 内部包含的是 List 组件,该组件比较特殊,它会默认响应手势的滑动操作,在处理外层滑动的时候该 List 也会跟着一起滑动,这种体验是非常不友好的,因此可以在 ListonScrollBegin() 方法中处理滑动冲突,简化代码如下所示:
@Component struct RefreshList 

  build() 
    Column() 
      Row()
      .id("refresh_header")
      .position(
        x: 0,
        y: this.offsetY
      )

      Column() 
        List(scroller: this.listScroller) 
        
        .edgeEffect(EdgeEffect.None)
        .onScrollBegin((dx: number, dy: number) =>  // 处理滑动冲突
          dy = this.listScrollable ? dy : 0;
          return dxRemain: dx, dyRemain: dy
        )
      
      .id("refresh_content")
      .position(
        x: 0,
        y: this.offsetY + this.refreshHeaderHeight
      )
    
    .id("refresh_list")
  

listScrollable 属性表示 List 是否可以滚动,当在处理外部滑动的时候禁止内部的 List 滑动,此时让 onScrollBegin() 方法返回的 dyRemain0 即可。

  • 完整代码
export namespace refresh 

  export class Constant 
    static readonly REFRESH_PULL_TO_REFRESH = "下拉刷新";
    static readonly REFRESH_FREE_TO_REFRESH = "释放立即刷新";
    static readonly REFRESH_REFRESHING      = "正在刷新";
    static readonly REFRESH_SUCCESS         = "刷新成功";
  

  @Component
  export struct RefreshList 

    @BuilderParam
    itemLayout?: (item: any, index: number) => any;

    @Watch("notifyRefreshingChanged")
    @Link refreshing: boolean;
    @Link dataSet: Array<any>;

    onRefresh?: () => void;
    onStatusChanged?: (status: RefreshStatus) => void;

    private headHeight: number = 55;
    private lastX: number = 0;
    private lastY: number = 0;
    private downY: number = 0;

    private flingFactor: number = 0.75;
    private touchSlop: number = 2;
    private offsetStep: number = 10;
    private intervalTime: number = 20;
    private listScrollable: boolean = true;
    private dragging: boolean = false;

    private refreshStatus: RefreshStatus = RefreshStatus.Inactive;

    @Watch("notifyOffsetYChanged")
    @State offsetY: number = -this.headHeight;

    @State refreshHeadIcon: Resource = $r("app.media.icon_refresh_down");
    @State refreshHeadText: string = refresh.Constant.REFRESH_PULL_TO_REFRESH;
    @State refreshContentH: number = 0;
    @State touchEnabled: boolean = true;
    @State headerVisibility: Visibility = Visibility.None;

    private listScroller: Scroller = new Scroller();

    private notifyRefreshingChanged() 
      if (this.refreshing) 
        this.showRefreshingStatus();
       else 
        this.finishRefresh();
      
    

    private notifyOffsetYChanged() 
      this.headerVisibility = (this.offsetY == -this.headHeight) ? Visibility.None : Visibility.Visible;
    

    @Builder headLayout() 
      Row() 
        Blank()
        Image(this.refreshHeadIcon)
          .width(30)
          .aspectRatio(1)
          .objectFit(ImageFit.Contain)

        Text(this.refreshHeadText)
          .fontSize(16)
          .width(150)
          .textAlign(TextAlign.Center)

        Blank()
      
      .width("100%")
      .height(this.headHeight)
      .backgroundColor("#44bbccaa")
      .visibility(this.headerVisibility)
      .position(
        x: 0,
        y: this.offsetY
      )
    

    build() 
      Column() 
        this.headLayout()

        Column() 
          List(scroller: this.listScroller) 
            if (this.dataSet) 
              ForEach(this.dataSet, (item, index) => 
                ListItem() 
                  if (this.itemLayout) 
                    this.itemLayout(item, index)
                  
                
                .width("100%")
              , item => item)
            
          
          .width("100%")
          .height("100%")
          .edgeEffect(EdgeEffect.None)
          .onScrollBegin((dx: number, dy: number) => 
            dy = this.listScrollable ? dy : 0;
            return dxRemain: dx, dyRemain: dy
          )
        
        .width("100%")
        .layoutWeight(1)
        .backgroundColor(Color.Pink)
        .position(
          x: 0,
          y: this.offsetY + this.headHeight
        )

      
      .width("100%")
      .height("100%")
      .enabled(this.touchEnabled)
      .onAreaChange((oldArea, newAre) => 
        console.log("Refresh height: " + newAre.height);
        this.refreshContentH = Number(newAre.height);
      )
      .clip(true)
      .onTouch((event) => 
        if (event.touches.length != 1) 
          this.logD("TOUCHES LENGTH INVALID: " + JSON.stringify(event.touches))
          event.stopPropagation();
          return
        
        switch (event.type) 
          case TouchType.Down:
            this.onTouchDown(event);
            break;
          case TouchType.Move:
            this.onTouchMove(event);
            break;
          case TouchType.Up:
          case TouchType.Cancel:
            this.onTouchUp(event);
            break;
        
        event.stopPropagation();
      )
    

    private setRefreshStatus(status: RefreshStatus) 
      this.refreshStatus = status;
      this.refreshing = (status == RefreshStatus.Refresh);
      this.touchEnabled = (status != RefreshStatus.Refresh && status != RefreshStatus.Done);
      this.notifyStatusChanged();
    

    private canRefresh() 
      return this.listScroller.currentOffset().yOffset == 0;
    

    private onTouchDown(event: TouchEvent) 
      this.lastX = event.touches[0].screenX;
      this.lastY = event.touches[0].screenY;
      this.downY = this.lastY;
      this.dragging = false;
      this.listScrollable = true;

      this.logD("Touch DOWN: " + event.touches[0].screenX.toFixed(2) + " x " + event.touches[0].screenY.toFixed(2) + ", offset: " + this.offsetY);
    

    private onTouchMove(event: TouchEvent) 
      let currentX = event.touches[0].screenX;
      let currentY = event.touches[0].screenY;
      let deltaX = currentX - this.lastX;
      let deltaY = currentY - this.lastY;
      if (this.dragging) 
        this.logD("offsetY: " + this.offsetY.toFixed(2) + ",  head: " + (-this.headHeight));
        if (deltaY < 0) 
          if (this.offsetY > -this.headHeight) 
            this.offsetY = this.offsetY + px2vp(deltaY) * this.flingFactor;
            this.listScrollable = false;
           else 
            this.offsetY = -this.headHeight;
            this.listScrollable = true;
            this.downY = this.lastY;
          
         else 
          if (this.canRefresh()) 
            以上是关于ArkUI实战,自定义下拉刷新组件RefreshList的主要内容,如果未能解决你的问题,请参考以下文章

ArkUI实战,自定义饼状图组件PieChart

下拉刷新上拉加载实战:带你理解自定义View整个过程

下拉刷新上拉加载实战:带你理解自定义View整个过程

自定义下拉刷新组件

HarmonyOSArkUI鸿蒙ArkUI开发框架ets开发中如何自定义组件

Qml自定义组件 - ListView下拉刷新 - PullToRefreshHandler