步步前行(RecyclerView拆分解析)-RecyclerView之TouchEvent篇

不喜勿喷,有错请留言(以下源码均来自recyclerView-27.1.1版本)

在之前的layout篇中,我们分析了layout的对ItemView进行创建,填充,测量和布局,以及锚点的意义等,接下来就要从scroll角度分析,但是谈及scroll就不得不提事件分发,故衍生出这一篇划水篇

RecyclerView的scroll操作

RecyclerView本身的scroll操作有多个,既然是滑动操作自然离不开我们的触摸事件处理,那我也决定先从触摸事件开始分析处理,循序渐进的的进入scroll,从而分析理解scroll角度里的layoutManager

首先我们RecyclerView中提供的scroll方法有很多scrollTo/scrollBy/smoothScrollToPosition/scrollToPosition以及jumpToPositionForSmoothScroller,但是这些方法的实现几乎都是依赖于layoutmanager,我逐个将其方法贴出

这里除了scrollTo方法体空实现,内部官方给了`RecyclerView does not support scrolling to an absolute position. Use scrollToPosition instead` 即使用scrollToPosition替代的说明
其他均是layoutManager实现
public void scrollToPosition(int position) {
        ...省略部分代码...
        mLayout.scrollToPosition(position);调用layoutManager的scrollToPosition操作
        awakenScrollBars();
    }

public void smoothScrollToPosition(int position) {
        ...省略部分代码...
        mLayout.smoothScrollToPosition(this, mState, position);调用layoutManager的smoothScrollToPosition操作
    }


 public void scrollBy(int x, int y) {
        ...省略部分代码...
        if (canScrollHorizontal || canScrollVertical) {
          虽然此处执行的是scrollByInternal方法,文章中会分析该方法,最终执行的是layoutManager的scrollHorizontallyBy/scrollVerticallyBy方法
            scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0, null);
        }
    }
    
 void jumpToPositionForSmoothScroller(int position) {
       ...省略部分代码...
        mLayout.scrollToPosition(position);调用layoutManager的scrollToPosition操作
        awakenScrollBars();
    }
   
    

onTouchEvent

首先由于onTouchEvent的方法较长,这里进行拆开分析,首先我们先进入onTouchEvent的时候就做了一些判断,我们先来看看这些条件

1.这第一个条件是布局是否被禁用,若被禁用时,则不做处理直接返回
if (mLayoutFrozen || mIgnoreMotionEventTillDown) {
    return false;
}
2.当布局未被禁用时,先看item是否有touch监听,如果有touch事件的监听处理,则将事件传递给item并重置触摸事件的标记
if (dispatchOnItemTouch(e)) {
    cancelTouch();
    return true;
}
3.若布局layoutManager为null,那就没发玩了,就直接返回 不处理
if (mLayout == null) {
    return false;
}
4.检查layout都支持那些方面的滚动,是否支持横向,是否支持纵向
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();

当判断完这几个条件后,就可以进行处理对触摸的不同事件的处理等操作

  • 准备工作

    1.创建一个辅助跟踪触摸事件速度的工具类
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    boolean eventAddedToVelocityTracker = false;
    2.从原有的motionEvent拷贝一份
    final MotionEvent vtev = MotionEvent.obtain(e);
    3.获取当前motinoEvent的类型,和关联的指针索引信息
    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();
    
    if (action == MotionEvent.ACTION_DOWN) {
        mNestedOffsets[0] = mNestedOffsets[1] = 0;
    }
    4.设置事件偏移量位置
    vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
    
  • 事件状态处理

    在这里插入图片描述

    • ACTION_DOWN 和 ACTION_POINTER_DOWN:

      由于它们两个比较接近,所以放在一块分析

      1.设置pointer

      2.初始化触摸位点

      3.设置方向滚动指示(ACTION_DOWN独有)

    在这里插入图片描述

    • ACTION_POINTER_UP

      POINTER_UP事件相对简单,重新选择新的指针,重置touchX和touchY

      private void onPointerUp(MotionEvent e) {
              final int actionIndex = e.getActionIndex();
              if (e.getPointerId(actionIndex) == mScrollPointerId) {
                  选择一个新的指针来弥补不足并重置touchX和touchY
                  final int newIndex = actionIndex == 0 ? 1 : 0;
                  mScrollPointerId = e.getPointerId(newIndex);
                  mInitialTouchX = mLastTouchX = (int) (e.getX(newIndex) + 0.5f);
                  mInitialTouchY = mLastTouchY = (int) (e.getY(newIndex) + 0.5f);
              }
          }
      
    • ACTION_UP

      1.通过VelocityTracker将用户的移动添加到跟踪器

      2.获取x轴与y轴的距离

      3.执行fling操作并重置scrollstate(由于这一步比较重要,我们再贴下其的位置,以免大家看不明白)

      if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                          setScrollState(SCROLL_STATE_IDLE);
                      }
      

      4.重置touch期间的一些工具和参数恢复初始化状态等待下一次touchEvent使用

    • ACTION_CANCEL

      从方法名上我们很容易知道这是一个取消触摸的方法

    • ACTION_MOVE

      在这里插入图片描述

      1.通过findPointerIndex找到其index,若index小于0,则跳过不处理

      2.通过xy的进一法,对x和y进行记录

      3.如果有嵌套滚动,则分发嵌套滚动至嵌套进程上

      在这里插入图片描述

      若滚动状态不是SCROLL_STATE_DRAGGING

      ​ 1.如果可以横向滚动,且x轴距离属于合理范围,则对x进行方向上的计算,并打开startScroll开关

      ​ 2.如果可以纵向滚动,且y轴距离属于合理范围,则对y进行方向上的计算,并打开startScroll开关

      ​ 3.如果startScroll开启,那么设置scrollState状态为SCROLL_STATE_DRAGGING

      在这里插入图片描述

      若滚动状态为SCROLL_STATE_DRAGGING

      ​ 1.srollByInternal操作,也将是我们layoutmanager滚动的开始(此方法我们留在篇尾重点分析)

      ​ 2.Gapworker进行预取操作

  • 事件处理简要分析

    可能有很多人对这块的事件处理有些许疑问

    • 为什么要监听ACTION_POINTER_DOWN和ACTION_POINTER_UP

      当我们用两根手指触摸屏幕时,只会根据第一根手指的滑动,当松开第一根手指时,如果我们不坚挺ACTION_POINTER的事件,会导致屏幕突然滚动一大段,就是缘于第二根手指移动事件的x,y会和第一根手指移动时留下的x和y比较导致屏幕滚动

scrollByInternal分析

​ 既然这个方法关联了我们layoutManager的滚动启动,那么我们就花篇幅分析,为我们下一篇的layoutManager做准备,我们一步步分析,由于scrollByInternal方法过长,我们先分析其前半部分

在这里插入图片描述

​ 首先先定义了针对x,y两个方向上的变量,紧接着就进入consumePendingUpdateOperations方法中,对于consumePendingUpdateOperations方法从方法命名上我们能感觉出来该方法主要作用处理延迟更新操作

void consumePendingUpdateOperations() {
  非第一次layoutComplete 或者 layout后数据发生改变
        if (!mFirstLayoutComplete || mDataSetHasChangedAfterLayout) {
            TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
            dispatchLayout();执行layoutstep操作
            TraceCompat.endSection();
            return;
        }
        if (!mAdapterHelper.hasPendingUpdates()) {
            return;
        }

  如果仅一个条目改变,检测任何可见项是否受影响,如果没有,就忽略该更改
        if (mAdapterHelper.hasAnyUpdateTypes(AdapterHelper.UpdateOp.UPDATE) && !mAdapterHelper
                .hasAnyUpdateTypes(AdapterHelper.UpdateOp.ADD | AdapterHelper.UpdateOp.REMOVE
                        | AdapterHelper.UpdateOp.MOVE)) {
            TraceCompat.beginSection(TRACE_HANDLE_ADAPTER_UPDATES_TAG);
            startInterceptRequestLayout();
            onEnterLayoutOrScroll();锁定滚动
            mAdapterHelper.preProcess();
            if (!mLayoutWasDefered) {
                if (hasUpdatedView()) {
                    dispatchLayout();执行layoutstep操作
                } else {
                  	不需要滚动,清除状态
                    mAdapterHelper.consumePostponedUpdates();
                }
            }
            stopInterceptRequestLayout(true);
            onExitLayoutOrScroll();释放滚动
            TraceCompat.endSection();
        } else if (mAdapterHelper.hasPendingUpdates()) {
            TraceCompat.beginSection(TRACE_ON_DATA_SET_CHANGE_LAYOUT_TAG);
            dispatchLayout();
            TraceCompat.endSection();
        }
    }

处理完延迟更新操作后,若adapter不为null

1.拦截requestLayout操作,然后锁定滚动状态(主要是为了推迟layout操作)

2.fillRemianingScrllValues 根据滚动状态,借助scroller查看可滚动的值还有多少

在这里插入图片描述

3.接着根据x和y不为0时,对layoutmanager进行scroll处理

4.最后释放滚动状态,释放拦截requestLayout操作

5.如果decoration不为null则通知invalidate掉decoration

这是scrollByInternal方法上半部分的处理,接下来我们看其下半部分的方法

在这里插入图片描述

  • 这一部分首先是执行了dispatchNestedScroll方法,这个方法内部是调用了NestedScrollingChildHelper的dispatchNestedScroll的方法,这个方法是对嵌套的处理,如果父级使用了任何嵌套滚动,则为true,其方法体会将滚动的x,y向父级处理

  • 接下来 对滚动状态,若不是View.OVER_SCROLL_NEVER就进行considerReleasingGlowsOnScroll,首先对于OverScrollMode的结果有三种第一种是OVER_SCROLL_ALWAYS(若视图允许滚动,始终允许过度滚动此视图)也是默认情况,第二种是OVER_SCROLL_IF_CONTENT_SCROLLS(若视图允许滚动,则当视图大于容器时,允许过度滚动),第三种就是OVER_SCROLL_NEVER(拒绝滚动此视图);可能有人对这里有些迷糊,我来大白话一下,细心的朋友发现Android2.3以后滚动的边界时,recylcerView会有一个光晕提示到底,其实这个就是对光晕的操作,如下:

    • View.OVER_SCROLL_ALWAYS

      滑动到边界继续滑动总是会出现提示的光晕

    • View.OVER_SCROLL_NEVER

      滑动道边界后,接续滑动永远不会出现提示的光晕

    • View.OVER_SCROLL_IF_CONTENT_SCROLLS

      如果内容大于容器,滑到边界会出现提示的光晕,反之则不会出现提示的光晕

  • 最后若consumeX和consumeY有其一不为0,就执行dispatchOnScrolled操作,其实就是滚动回调操作,方法如下

    在这里插入图片描述

    • 传递当前的scrollx和scrolly,通过onScrolled传出,以便响应视图内部滚动处理
    • 如果scrollListener不为null则将其操作通过onScrollChanged传出
  • 是否唤醒scrollBar,若唤醒则执行invalidate操作

至此,我们的onTouch的部分就分析完毕了,

onInterceptTouchEvent

在这里插入图片描述

  • 还是老规矩,首先看布局是否允许滚动,在然后就是itemTouchListerner是否拦截消费,重置touch状态,不再处理

    在这里插入图片描述

    若mOnItemTouchListners的size大于0则执行listener的onInterceptTouchEvent方法

  • 接着如果layout为null不处理,然后判断layout支持横向还是竖向

  • 借助VelocityTracker来处理滑动速度

  • 接着就是事件的分发处理(由于与touchEvent比较类似所以就不再分析,具体可借鉴前方的touchEvent分析)

至此我们的RecyclerView的touchEvent事件篇就到这结束了,从这里我们可以看出对于touch事件,recyclerView自身对于scroll状态以及嵌套情况的处理,若有不对,敬请批评指出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章