理解RecyclerView(六)—RecyclerView的滑动原理

前言:当你感到不舒服的时候就是成长的时候。我爱这艰难又拼尽全力的每一天。

一、概述

  RecyclerView作为一个列表控件,自带滑动功能,实际开发中经常用到,它的滑动原理也是我们需要掌握的,正所谓“知其然更要知其之所然”。RecyclerView的滑动事件处理依然是通过onTouchEvent()触控事件响应的,不同的是RecyclerView采用嵌套滑动机制,会把滑动事件通知给支持嵌套滑动的父View先做决定。本文在介绍普通滑动的过程中可能会涉及到嵌套滑动的知识(下篇文章会分析嵌套滑动),先来看看普通滑动的效果图:
理解RecyclerView(六)—RecyclerView的滑动原理_第1张图片

温馨提示:本文源码基于androidx.recyclerview:recyclerview:1.2.0-alpha01

二、onTouchEvent()

  RecyclerView的事件处理依然是通过onTouchEvent()触控事件响应的,这里补充一点onTouchEvent()的知识,熟悉的可以忽略。

  • boolean onTouchEvent(MotionEvent event)  实现此方法来处理触摸屏运动事件,返回值true表示处理事件,false表示不处理事件;
  • MotionEvent.ACTION_DOWN   手指按下,一个按下的手势已经开始,该动作包括初始的起始位置;
  • MotionEvent.ACTION_MOVE   手指移动,在按下手势时(在down和up之间)发生了改变,该运动包含最近的点,以及自上次向下或移动事件以来的任何中间点;
  • MotionEvent.ACTION_UP     手指离开,一个按下的手势已经完成,该动作包含一个最终的发布位置以及自上一个向下或移动事件以来的任何中间点;
  • MotionEvent.ACTION_CANCEL 手势取消,当前手势已经终止,你不会得到更多的坐标点,可以将此视为up事件,但不执行任何你通常会执行的操作;
  • MotionEvent.ACTION_POINTER_DOWN  多个手指按下,一个非主触摸点在下降;
  • MotionEvent.ACTION_POINTER_UP    多个手指离开,一个非主触摸点上升;
  • MotionEvent.ACTION_OUTSIDE     手指触碰超出了正常边界,移动发生在UI元素的正常范围之外。这并不是提供一个完整的手势,但只是提供了运动触摸的初始位置;注意,因为任何事件的位置都在视图层次结构的边界之外,所以默认情况它不会被分配给ViewGroup的任何子元素;
  • MotionEvent.ACTION_SCROLL     非触摸滑动,运动事件包含相对的垂直/水平滚动偏移量,这个动作不是触摸事件。

先来看看RecyclerView的onTouchEvent()方法:

  	@Override
    public boolean onTouchEvent(MotionEvent e) {
     	//将滑动事件分派给OnItemTouchListener或为OnItemTouchListeners提供拦截机会,触摸事件被拦截处理则返回true
     	if (findInterceptingOnItemTouchListener(e)) {
            cancelScroll();
            return true;
        }
        
     	//根据布局方向来决定滑动的方向
        final boolean canScrollHorizontally = mLayout.canScrollHorizontally();//能否支持水平方向滑动
        final boolean canScrollVertically = mLayout.canScrollVertically();//能否支持垂直方向滑动
       	
       	//获取一个新的VelocityTracker对象来观察滑动的速度
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(e);
		//返回正在执行的操作,不包含触摸点索引信息。即事件类型,如MotionEvent.ACTION_DOWN
        final int action = e.getActionMasked();
        final int actionIndex = e.getActionIndex();//Action的索引
        //复制事件信息创建一个新的事件
		final MotionEvent vtev = MotionEvent.obtain(e);
        vtev.offsetLocation(mNestedOffsets[0], mNestedOffsets[1]);
        
        switch (action) {
            case MotionEvent.ACTION_DOWN: {//手指按下
             	mScrollPointerId = e.getPointerId(0);//特定触摸点相关联的触摸点id,获取第一个触摸点的id
             	//记录down事件的X、Y坐标
                mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
                
                if (canScrollHorizontally) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;//指示沿水平轴滑动
                }
                if (canScrollVertically) {
                    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;//指示沿纵轴滑动
                }
                //开启一个新的嵌套滚动,如果找到一个协作的父View,并开始嵌套滑动
                startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            } break;
            
  			case MotionEvent.ACTION_POINTER_DOWN: {//多个手指按下
  				//更新mScrollPointerId,表示只会响应最近按下的手势事件
                mScrollPointerId = e.getPointerId(actionIndex);
                //更新最近的手势坐标
                mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
                mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
               } break;
                
            case MotionEvent.ACTION_MOVE: {//手指移动
            	//根据mScrollPointerId获取触摸点下标
             	final int index = e.findPointerIndex(mScrollPointerId);
              
              	//根据move事件产生的x,y来计算偏移量dx,dy 
                final int x = (int) (e.getX(index) + 0.5f);
                final int y = (int) (e.getY(index) + 0.5f);
                int dx = mLastTouchX - x;
                int dy = mLastTouchY - y;
				
                if (mScrollState != SCROLL_STATE_DRAGGING) {//不是被触摸移动状态
                    boolean startScroll = false;
                    if (canScrollHorizontally) {//水平滑动的方向
                        if (dx > 0) {
                            dx = Math.max(0, dx - mTouchSlop);
                        } else {
                            dx = Math.min(0, dx + mTouchSlop);
                        }
                        if (dx != 0) {
                            startScroll = true;
                        }
                    }
                    if (canScrollVertically) {//垂直滑动的方向
                        if (dy > 0) {
                            dy = Math.max(0, dy - mTouchSlop);
                        } else {
                            dy = Math.min(0, dy + mTouchSlop);
                        }
                        if (dy != 0) {
                            startScroll = true;
                        }
                    }
                    if (startScroll) {
                        setScrollState(SCROLL_STATE_DRAGGING);
                    }
                }
				//被触摸移动状态,真正处理滑动的地方
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    mReusableIntPair[0] = 0;//mReusableIntPair父view消耗的滑动距离
                    mReusableIntPair[1] = 0;
					//mScrollOffset表示RecyclerView的滚动位置
					//将嵌套的预滑动操作的一个步骤分派给当前嵌套的滑动父View,如果为true表示父View优先处理滑动事件。
					//如果消耗,dx dx会分别减去父View消耗的那一部分距离
                    if (dispatchNestedPreScroll(
                            canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
                            mReusableIntPair, mScrollOffset, TYPE_TOUCH
                    )) {
                        dx -= mReusableIntPair[0];//减去父View消耗的那一部分距离
                        dx -= mReusableIntPair[1];
                        //更新嵌套的偏移量
                        mNestedOffsets[0] += mScrollOffset[0];
                        mNestedOffsets[1] += mScrollOffset[1];
                        //滑动已经开始,防止父View被拦截
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                    mLastTouchX = x - mScrollOffset[0];
                    mLastTouchY = y - mScrollOffset[1];
					//最终实现的滑动效果
                    if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    //从缓存中预取一个ViewHolder
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            } break;

            case MotionEvent.ACTION_POINTER_UP: {//多个手指离开
            	//选择一个新的触摸点来处理结局,重新处理坐标
                onPointerUp(e);
            } break;

            case MotionEvent.ACTION_UP: {//手指离开,滑动事件结束
             	mVelocityTracker.addMovement(vtev);
                eventAddedToVelocityTracker = true;
                //计算滑动速度
                mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
                //最后一次 X/Y 轴的滑动速度
                final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
                final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
                 //处理惯性滑动
                if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
                    setScrollState(SCROLL_STATE_IDLE);//设置滑动状态
                }
                resetScroll();//重置滑动
            } break;
            
            case MotionEvent.ACTION_CANCEL: {//手势取消,释放各种资源
                cancelScroll();//退出滑动
            } break;
        }
        
        if (!eventAddedToVelocityTracker) {
            mVelocityTracker.addMovement(vtev);
        }
        vtev.recycle();//回收滑动事件,方便重用,调用此方法你不能再接触事件
        
        return true;//返回true表示由RecyclerView来处理事件
    }

上面就是RecyclerView的onTouchEvent()方法,其中ACTION_DOWNACTION_MOVEACTION_UPACTION_CANCEL这几个事件是View的基本事件,ACTION_POINTER_DOWNACTION_POINTER_UP这个两个事件跟多指滑动有关。

这里主要做了三件事,一是将滑动事件分派给OnItemTouchListener或为OnItemTouchListeners提供拦截机会,被拦截处理则返回true,即消费掉事件;二是初始化手势坐标,滑动方向,事件信息等数据;三是OnItemTouchListener或OnItemTouchListeners不消费当前事件,那么走正常的事件分发流程。这里面有很多细节,我们逐个事件来详细分析:

2.1 Down事件

    case MotionEvent.ACTION_DOWN:{//手指按下
        mScrollPointerId = e.getPointerId(0);//特定触摸点相关联的触摸点id,获取第一个触摸点的id
        //1.记录down事件的X、Y坐标
        mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
        mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

        if (canScrollHorizontally) {
            nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;//指示沿水平轴滑动
        }
        if (canScrollVertically) {
            nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;//指示沿纵轴滑动
        }
        //2.开启一个新的嵌套滑动,如果找到一个协作的父View,并开始嵌套滚动
        startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
    } break;

Down事件首先获取第一个触摸点id,一个Pointer就是一个触摸点,down是一系列事件的开始,这里主要做了两件事:

  • 1.记录down事件的X,Y坐标;
  • 2.调用startNestedScroll()启一个新的嵌套滑动,如果找到嵌套的父View则会启动嵌套滑动,即处理事件。

2.2 Move事件

   case MotionEvent.ACTION_MOVE:{//手指移动
   		//根据mScrollPointerId获取触摸点下标
        final int index = e.findPointerIndex(mScrollPointerId);

        //1.根据move事件产生的x,y来计算偏移量dx,dy 
        final int x = (int) (e.getX(index) + 0.5f);
        final int y = (int) (e.getY(index) + 0.5f);
        int dx = mLastTouchX - x;
        int dy = mLastTouchY - y;

        if (mScrollState != SCROLL_STATE_DRAGGING) {//不是被触摸移动状态
            boolean startScroll = false;
            if (canScrollHorizontally) {//水平滑动的方向
                ······
            }
            if (canScrollVertically) {//垂直滑动的方向
               ······
            }
            //设置滑动状态,SCROLL_STATE_DRAGGING表示正在滑动中
            if (startScroll) setScrollState(SCROLL_STATE_DRAGGING);
        }
        //被触摸移动状态,真正处理滑动的地方
        if (mScrollState == SCROLL_STATE_DRAGGING) {
            mReusableIntPair[0] = 0;//mReusableIntPair父view消耗的滑动距离
            mReusableIntPair[1] = 0;
            //2.将嵌套的预滑动操作的一个步骤分派给当前嵌套的滚动父View,如果为true表示父View优先处理滑动事件。
            //如果消耗,dx dy会分别减去父View消耗的那一部分距离,mScrollOffset表示RecyclerView的滚动位置
            if (dispatchNestedPreScroll(
                    canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0,
                    mReusableIntPair, mScrollOffset, TYPE_TOUCH
            )) {
                dx -= mReusableIntPair[0];//减去父View消耗的那一部分距离
                dy -= mReusableIntPair[1];
                //更新嵌套的偏移量
                mNestedOffsets[0] += mScrollOffset[0];
                mNestedOffsets[1] += mScrollOffset[1];
                //开始滑动,防止父View被拦截
                getParent().requestDisallowInterceptTouchEvent(true);
            }

            mLastTouchX = x - mScrollOffset[0];
            mLastTouchY = y - mScrollOffset[1];
            //3.最终实现的滚动效果
            if (scrollByInternal(canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, e)) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            //4.从缓存中预取一个ViewHolder
            if (mGapWorker != null && (dx != 0 || dy != 0)) {
                mGapWorker.postFromTraversal(this, dx, dy);
            }
        }
    } break;

Move事件是处理滑动事件的核心,代码比较长,但是结构简单,主要分为如下几步:

  • 1.根据move事件产生的x、y计算偏移量dx,dy;
  • 2.dispatchNestedPreScroll()分派一个步骤询问父View是否需要先处理滑动事件,如果处理则dx,dy会分别减去父View消耗的那一部分距离;
  • 3.判断滑动方向,调用scrollByInternal()最终实现滚动效果;
  • 4.调用mGapWorker.postFromTraversal()从RecyclerView缓存中预取一个ViewHolder。

scrollByInternal()是最终实现滑动效果,后面会详细分析,GapWorker预取ViewHolder是通过添加Runnable到RecyclerView任务队列中,最终调用RecyclerView.RecyclertryGetViewHolderForPositionByDeadline()获取ViewHolder,它是整个RecyclerView回收复用缓存机制的核心方法。这里就不详细分析了,《RecyclerView的回收复用缓存机制详解》希望能给你提供帮助。

2.3 Up事件

   case MotionEvent.ACTION_UP: {//手指离开,滑动事件结束
        mVelocityTracker.addMovement(vtev);
        eventAddedToVelocityTracker = true;
        //1.根据过去的点计算现在的滑动速度
        mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
        //最后一次 X/Y 轴的滑动速度
        final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
        final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
        //处理惯性滑动
        if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
            setScrollState(SCROLL_STATE_IDLE);//设置滑动状态
        }
        resetScroll();//2.重置滑动
    } break;

Up事件在手指离开后,滑动事件结束。主要做了两件事:

  • 1.通过computeCurrentVelocity()计算滑动的速度以及计算X,Y轴的最后滑动速度,fling()是处理惯性滑动;
  • 2.惯性滑动结束后设置滑动状态,重置滑动信息。

先通过computeCurrentVelocity()计算滑动的速度以及计算X,Y轴最后的滑动速度后,如果抬起的时候最后速度大于系统的给定值,就保持惯性再滑动一段距离,最后通知嵌套滑动的View滑动结束,重置数据。fling()是处理惯性滑动的核心方法,下面会分析到。

2.4 Cancel事件

    case MotionEvent.ACTION_CANCEL:{//手势取消,释放各种资源
        cancelScroll();//退出滑动
    } break;
    
 	private void cancelScroll() {
 		//1.重置滑动,是否资源
        resetScroll();
        //2.设置滑动状态为没有滑动状态
        setScrollState(SCROLL_STATE_IDLE);
    }

Cancel事件表示手势事件被取消了,重置滑动状态等信息。主要做了两件事:

  • 1.resetScroll()停止正在进行的嵌套滑动,释放资源;
  • 2.设置滑动状态为SCROLL_STATE_IDLE没有滑动。

当事件中途被父View消费时会响应cancel事件,比如在RecyclerView接收到down事件,但是后续被父View拦截,RecyclerView就会响应cancel事件。

2.5 Pointer_Down事件

    case MotionEvent.ACTION_POINTER_DOWN:{//多个手指按下
        //更新mScrollPointerId,表示只会响应最近按下的手势事件
        mScrollPointerId = e.getPointerId(actionIndex);
        //更新最近的手势坐标
        mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
        mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
    } break;

Pointer_Down事件主要是在多个手指按下时,立即更新mScrollPointerId和按下的坐标。响应新的手势,不再响应旧的手势,一切事件和坐标以新的事件和坐标为准。

注意:这里多指滑动的意思不是RecyclerView响应多个手指滑动,而是当旧的一个手指没有释放时,此时另一个新的手指按下,那么RecyclerView就不响应旧手指的手势,而是响应最新手指的手势。

2.6 Pointer_Up事件

    case MotionEvent.ACTION_POINTER_UP:{//多个手指离开
        //选择一个最新的坐标点来处理结局,重新处理坐标
        onPointerUp(e);
    } break;
    
   private void onPointerUp(MotionEvent e) {
        final int actionIndex = e.getActionIndex();
        if (e.getPointerId(actionIndex) == mScrollPointerId) {
            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);
        }
    }

Pointer_Up事件在多指离开时,选择一个最新的指针来处理结局。onPointerUp()判断离开的事件坐标id是否与当前的滑动坐标id一致,如果一致则更新手势坐标和当前坐标点id。

三、滑动流程

  那么RecyclerView的onTouchEvent()方法相关的事件类型分析完了,下面看看RecyclerView在处理自身滑动时究竟做了什么?上一篇文章结合LinearLayoutManager的源码分析了RecyclerView的绘制流程,这里同样以LinearLayoutManager的垂直方向为例分析RecyclerView垂直方向的滑动,其他方式的滑动都是万变不离其宗。开始响应onTouchEvent()方法时获取滑动方向:

	//根据布局方向来决定滑动的方向
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();//能否支持水平方向滑动
    final boolean canScrollVertically = mLayout.canScrollVertically();//能否支持垂直方向滑动

上面是通过LinearLayoutManager获取是否能水平、垂直方向滑动,这里回调了mLayout.canScrollHorizontally()mLayout.canScrollVertically()方法,如果canScrollHorizontally = ture时,能左右滑动,如果canScrollVertically = true时,能上下滑动。

    @Override
    public boolean canScrollHorizontally() {
        return mOrientation == HORIZONTAL;//线性方向为水平方向,能水平滑动
    }

    @Override
    public boolean canScrollVertically() {
        return mOrientation == VERTICAL;//线性方向为垂直方向,能垂直滑动
    }

3.1 普通滑动

在上面的Move事件分析知道,在ACTION_MOVE里面计算滑动的距离,然后调用scrollByInternal()处理itemView随着手势的移动而滑动,核心方法是scrollByInternal()

   boolean scrollByInternal(int x, int y, MotionEvent ev) {
        int unconsumedX = 0; int unconsumedY = 0;
        int consumedX = 0; int consumedY = 0;
		//1.使用延迟更改来避免滑动期间adapter更改可能引发的崩溃
        consumePendingUpdateOperations();
        if (mAdapter != null) {
        	//2.滑动步骤
            scrollStep(x, y, mReusableIntPair);
            consumedX = mReusableIntPair[0];
            consumedY = mReusableIntPair[1];
            unconsumedX = x - consumedX;
            unconsumedY = y - consumedY;
        }
   		//3.将嵌套的预滑动操作的一个步骤分派给当前嵌套的滑动父View,如果为true表示父View优先处理滑动事件。
        //如果消耗,dx dy会分别减去父View消耗的那一部分距离
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,
                TYPE_TOUCH, mReusableIntPair);
        unconsumedX -= mReusableIntPair[0];
        unconsumedY -= mReusableIntPair[1];
        boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;

        //将滑动的偏移量考虑在内,更新最后的触摸坐标
        mLastTouchX -= mScrollOffset[0];
        mLastTouchY -= mScrollOffset[1];
        mNestedOffsets[0] += mScrollOffset[0];
        mNestedOffsets[1] += mScrollOffset[1];
		//4.滑动回调
        if (consumedX != 0 || consumedY != 0) {
            dispatchOnScrolled(consumedX, consumedY);
        }
    
        //是否有滑动消耗
        return consumedNestedScroll || consumedX != 0 || consumedY != 0;
    }

上面的代码主要做了四件事:

  • 1.consumePendingUpdateOperations(),使用延迟来避免滑动期间adapter更改可能引发的崩溃,因为滑动假定没有数据改变,但实际上数据已经更改;
  • 2.scrollStep()核心滑动步骤,交给布局管理器处理自身滑动;
  • 3.自身滑动完毕后仍采用嵌套的滑动机制通知父View优先处理滑动事件;
  • 4.dispatchOnScrolled()通知RecyclerView的滑动回调监听。

scrollStep()是处理自身滑动的方法,通过dx,dy来滑动RecyclerView,水平滑动则调用mLayout.scrollHorizontallyBy(),垂直滑动则调用mLayout.scrollVerticallyBy()

   void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        if (dx != 0) {
        	//在屏幕坐标中水平滑动dx像素,并返回移动的距离,默认不移动,返回为0
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
        	//在屏幕坐标中垂直滑动dy像素,并返回移动的距离,默认不移动,返回为0
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }
   }

最终滑动的距离由LayoutManager处理滑动函数,这里看垂直方向的滑动scrollVerticallyBy()

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
            RecyclerView.State state) {
        if (mOrientation == HORIZONTAL) {//如果是水平方向,则垂直方向的滑动距离为0,即不滑动
            return 0;
        }
        return scrollBy(dy, recycler, state);
    }

scrollVerticallyBy()是垂直方向的滑动,如果线性方向为HORIZONTAL,则滑动距离为0,即不滑动,否则调用scrollBy()实现垂直方向的滑动:

  int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
          if (getChildCount() == 0 || delta == 0) {//如果没有数据或者滑动距离为0,则不滑动
            return 0;
        }
        //1.更新布局状态
        updateLayoutState(layoutDirection, absDelta, true, state);
        //2.先调用fill()把滑进来的view布局进来,并回收滑出去的view,返回当前布局View的空间
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
    	//计算滑动的距离
        final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        //3.给所有子View添加偏移量,按照计算滑动的距离动距离移动View的位置
        mOrientationHelper.offsetChildren(-scrolled);//移动
        mLayoutState.mLastScrollDelta = scrolled;//记录本次滑动的距离
        return scrolled;
    }

这两个方法主要做三件事,

  • 1.通过updateLayoutState()修正了一些状态,比如描点在哪里,是否有动画等;
  • 2.通过fill()先检查有哪些view超出边界,进行回收,然后重新填充新的view,并返回填充的偏移量;
  • 3.通过offsetChildren()给所有子View添加偏移量,按照滑动距离移动View的位置

scrollBy()处理滑动的逻辑就是先更新布局的状态,然后调用fill()函数返回填充的距离,同时如果有滑动距离则把View布局进来,如果一个View被完全移出屏幕则回收到缓存中,最后计算滑动距离调用offsetChildren()给所有子view进行偏移。

注意:滑动事件并不会重新请求布局,不会重新onLayoutChildren(),对布局的更新是通过fill()重新从缓存获取或者创建一个itemView填充到屏幕。

我们来看看offsetChildren()移动itemView的方法,在OrientationHelper帮助类里面找到offsetChildren()的抽象方法,那么我们得去实现类中找到实现这个方法的逻辑:

	//通过给出的距离移动所有的子VIew
	public abstract void offsetChildren(int amount);

LinerLayoutManager源码里面实现了OrientationHelper帮助类并实现了抽象方法:

  public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        return new OrientationHelper(layoutManager) {
            @Override
            public void offsetChildren(int amount) {
                mLayoutManager.offsetChildrenVertical(amount);//沿垂直方向偏移所有子View附加到RecyclerView中
            }
        };
    }

offsetChildrenVertical(int dx)是沿垂直方向偏移所有子View附加到RecyclerView中,也是回调LayoutManager中的offsetChildrenVertical()

   public void offsetChildrenVertical(@Px int dy) {
      if (mRecyclerView != null) {
          mRecyclerView.offsetChildrenVertical(dy);
       }
   }

跟进去又回到RecyclerView的offsetChildrenVertical()

	public void offsetChildrenVertical(@Px int dy) {
		//获取RecyclerView的ItemView个数
        final int childCount = mChildHelper.getChildCount();
        //遍历所有ItemView,调用View的offsetTopAndBottom()进行滑动
        for (int i = 0; i < childCount; i++) {
            mChildHelper.getChildAt(i).offsetTopAndBottom(dy);//移动view多少像素
        }
    }

终于找到了核心移动子View的源码:遍历所有ItemView,最终通过每个子View调用了底层View的offsetTopAndBottom()或者offsetLeftAndRight()方法来实现滑动的。先获取到itemView的总数,然后通过遍历将每一个itemView移动指定的距离dy。

普通滑动总结:在RecyclerView的Move触摸事件分派滑动事件响应scrollByInternal()方法,处理父View嵌套滑动,实际上调用LayoutManager的scrollHorizontallyBy()或者scrollVerticallyBy()方法来计算scrollBy()fill()填充布局同时处理实际的滑动距离,遍历所有ItemView,最终通过每个子View调用了底层View的offsetTopAndBottom()或者offsetLeftAndRight()方法来实现滑动的。

3.2 惯性滑动

我们在快速滑动列表然后松开手指,列表依然会持续惯性滑动一段时间,RecyclerView的惯性滑动fling(),在onTouchEvent()处理ACTION_UP事件的时候:

 case MotionEvent.ACTION_UP: {//手指离开,滑动事件结束
        mVelocityTracker.addMovement(vtev);
        //1.根据过去的点计算现在的滑动速度
        mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
        //最后一次 X/Y 轴的滑动速度
        final float xvel = canScrollHorizontally ? -mVelocityTracker.getXVelocity(mScrollPointerId) : 0;
        final float yvel = canScrollVertically ? -mVelocityTracker.getYVelocity(mScrollPointerId) : 0;
        //处理惯性滑动
        if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
            setScrollState(SCROLL_STATE_IDLE);//设置滑动状态
        }
        resetScroll();//2.重置滑动
    } break;

先通过computeCurrentVelocity()计算滑动的速度以及计算X,Y轴最后的滑动速度,如果抬起的时候最后速度大于系统的给定值,就保持惯性再滑动一段距离,最后通知嵌套滑动的View滑动已结束,重置滑动信息。惯性滑动核心方法fling()

    public boolean fling(int velocityX, int velocityY) {
    	//1.能否水平、垂直方向滑动
        final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
        final boolean canScrollVertical = mLayout.canScrollVertically();

        if (!canScrollHorizontal || Math.abs(velocityX) < mMinFlingVelocity) {
            velocityX = 0;//如果不能水平滑动,或者滑动速度小于系统的滑动速度,则水平滑动速度设置为0
        }
        if (!canScrollVertical || Math.abs(velocityY) < mMinFlingVelocity) {
            velocityY = 0;//如果不能垂直滑动,或者滑动速度小于系统的滑动速度,则垂直滑动速度设置为0
        }
        
        //没有滑动速度,返回false,不处理惯性滑动
        if (velocityX == 0 && velocityY == 0) return false;

		//父View是否处理嵌套预惯性滑动
        if (!dispatchNestedPreFling(velocityX, velocityY)) {
            final boolean canScroll = canScrollHorizontal || canScrollVertical;
            dispatchNestedFling(velocityX, velocityY, canScroll);
			//2.客户端按照开发者需求自己处理惯性滑动
            if (mOnFlingListener != null && mOnFlingListener.onFling(velocityX, velocityY)) {
                return true;
            }

            if (canScroll) {//如果能滑动
                startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH);//开始嵌套滑动
                //3.RecyclerView自己处理惯性滑动
                mViewFlinger.fling(velocityX, velocityY);
                return true;
            }
        }
        return false;
    }

fling()这里主要做了三件事:

  • 1.先根据滑动方向以及滑动速度与系统速度比较,判断能不能惯性滑动;
  • 2.客户端按照开发者要求自己处理惯性滑动,通过OnFlingListener.onFling()方法判断RecyclerView是否优处理开发者要求的惯性运动,在决定本身是否处理惯性滑动;
  • 3.RecyclerView自己处理惯性滑动,调用ViewFlinger的fling()方法。

优先处理开发者的惯性滑动(这里就不分析了),如果开发者不处理则RecyclerView处理自身惯性滑动,ViewFlinger是RecyclerView内部的一个Runnable类,接着ViewFlinger的fling()

    public void fling(int velocityX, int velocityY) {
    	setScrollState(SCROLL_STATE_SETTLING);//设置滚动状态为惯性滑动
        mOverScroller = new OverScroller(getContext(), sQuinticInterpolator);
        //基于一个摇摆的手势开始滑动,所走的距离取决于初速度。
        mOverScroller.fling(0, 0, velocityX, velocityY,
                Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
        //使Runnable在下一个动画时间步上执行,runnable将在用户界面线程上运行。
        postOnAnimation();
    }

mOverScroller.fling()只是计算惯性滑动的相关参数,最后调用了postOnAnimation()方法,它最终回调ViewFlinger的run方法:

    @Override
    public void run() {
        ······
        final OverScroller scroller = mOverScroller;
        //1.更新滑动位置信息,判断当前是否滑动完毕,true表示为未滑动完毕
        if (scroller.computeScrollOffset()) {
            final int x = scroller.getCurrX();
            final int y = scroller.getCurrY();
            //计算滚动距离
            int unconsumedX = x - mLastFlingX;
            int unconsumedY = y - mLastFlingY;
            mLastFlingX = x;
            mLastFlingY = y;
            int consumedX = 0;
            int consumedY = 0;
         	······
            if (mAdapter != null) {//本地滑动
                mReusableIntPair[0] = 0;
                mReusableIntPair[1] = 0;
                //2.滑动步骤,通过dX,dY滑动RecyclerView
                scrollStep(unconsumedX, unconsumedY, mReusableIntPair);
                consumedX = mReusableIntPair[0];
                consumedY = mReusableIntPair[1];
                unconsumedX -= consumedX;
                unconsumedY -= consumedY;
            }

            //嵌套后滑动
            mReusableIntPair[0] = 0;
            mReusableIntPair[1] = 0;
            //父View是否处理嵌套滑动事件
            dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, null,
                    TYPE_NON_TOUCH, mReusableIntPair);
            unconsumedX -= mReusableIntPair[0];
            unconsumedY -= mReusableIntPair[1];
			
            boolean scrollerFinishedX = scroller.getCurrX() == scroller.getFinalX();
            boolean scrollerFinishedY = scroller.getCurrY() == scroller.getFinalY();
			//滑动是否完成(滑动结束或者x,y距离完成滑动或者无法进一步滑动)
            final boolean doneScrolling = scroller.isFinished()
                    || ((scrollerFinishedX || unconsumedX != 0)
                    && (scrollerFinishedY || unconsumedY != 0));

			//4.滑动结束
            if (!smoothScrollerPending && doneScrolling) {
                if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {
                    final int vel = (int) scroller.getCurrVelocity();
                    int velX = unconsumedX < 0 ? -vel : unconsumedX > 0 ? vel : 0;
                    int velY = unconsumedY < 0 ? -vel : unconsumedY > 0 ? vel : 0;
                    absorbGlows(velX, velY);
                }

                if (ALLOW_THREAD_GAP_WORK) {
                    mPrefetchRegistry.clearPrefetchPositions();
                }
            } else {
                //3.否则继续滑动(递归执行run方法,直到滑动结束为止)
                postOnAnimation();
                //预取ViewHolder(缓存中获取或者创建)
                if (mGapWorker != null) {
                    mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
                }
            }
        }
		//重新滑动
        if (mReSchedulePostAnimationCallback) {
            internalPostOnAnimation();
        } else {//设置滑动结束状态
            setScrollState(SCROLL_STATE_IDLE);
            stopNestedScroll(TYPE_NON_TOUCH);
        }
    }

上面主要做了三件事:

  • 1.更新滑动位置信息,判断当前是否滑动完毕,true表示为未滑动完毕;
  • 2.计算滑动距离的相关信息,回调scrollStep()通过dX,dY滑动RecyclerView;
  • 3.如果滑动未结束,执行postOnAnimation()递归run方法,直到滑动结束为止;
  • 4.滑动结束,清除数据,设置滑动结束状态。

惯性滑动总结:在RecyclerView响应onTouchEvent()的up事件时,根据最后滑动速度判断是否有惯性滑动,如果有则通过fling()先处理开发者要求处理的惯性滑动,否则直接RecyclerView自身处理惯性滑动,在ViewFlinger的fling()计算滑动相关的坐标数据信息,然后在postOnAnimation()中回调run()处理滑动,也是调用scrollStep()完成滑动,如果滑动未结束则递归执行postOnAnimation()方法回调run()直接滑动完成。

四、RecyclerView的滑动原理总结

  RecyclerView的滑动事件处理依然是通过onTouchEvent()触控事件响应,计算更新触摸坐标以及滑动方向等相关信息,处理父View的嵌套滑动,滑动事件响应scrollByInternal()方法,实际上调用LayoutManager的scrollHorizontallyBy()或者scrollVerticallyBy()方法来计算在scrollBy()fill()填充布局同时处理实际的滑动距离,最后RecyclerView遍历所有ItemView,最终通过每个子View调用了底层View的offsetTopAndBottom()或者offsetLeftAndRight()方法来实现滑动的。

RecyclerView的滑动流程图如下(双击点开更高清):
理解RecyclerView(六)—RecyclerView的滑动原理_第2张图片
至此,本文结束!


请尊重原创者版权,转载请标明出处:https://blog.csdn.net/m0_37796683/article/details/104951780 谢谢!


相关文章:

理解RecyclerView(五)

 ● RecyclerView的绘制流程

理解RecyclerView(六)

 ● RecyclerView的滑动原理

理解RecyclerView(七)

 ● RecyclerView的嵌套滑动机制

理解RecyclerView(八)

 ● RecyclerView的回收复用缓存机制详解

理解RecyclerView(九)

 ● RecyclerView的自定义LayoutManager

你可能感兴趣的:(RecyclerView系列)