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

前言:當你感到不舒服的時候就是成長的時候。我愛這艱難又拼盡全力的每一天。

一、概述

  RecyclerView作爲一個列表控件,自帶滑動功能,實際開發中經常用到,它的滑動原理也是我們需要掌握的,正所謂“知其然更要知其之所然”。RecyclerView的滑動事件處理依然是通過onTouchEvent()觸控事件響應的,不同的是RecyclerView採用嵌套滑動機制,會把滑動事件通知給支持嵌套滑動的父View先做決定。本文在介紹普通滑動的過程中可能會涉及到嵌套滑動的知識(下篇文章會分析嵌套滑動),先來看看普通滑動的效果圖:

二、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的滑動流程圖如下(雙擊點開更高清):
在這裏插入圖片描述
至此,本文結束!


請尊重原創者版權,轉載請標明出處:https://blog.csdn.net/m0_37796683/article/details/104951780 謝謝!


相關文章:

理解RecyclerView(五)

 ● RecyclerView的繪製流程

理解RecyclerView(六)

 ● RecyclerView的滑動原理

理解RecyclerView(七)

 ● RecyclerView的嵌套滑動機制

理解RecyclerView(八)

 ● RecyclerView的回收複用緩存機制詳解

理解RecyclerView(九)

 ● RecyclerView的自定義LayoutManager

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