RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關組件的源碼分析

  對於使用ReccyclerView的咱們來講,LayoutManager早已很是熟悉。但是,有沒有想過咱們所說的熟悉是哪一種熟悉?對的,就是會使用而已,這其中包括谷歌爸爸幫咱們實現的幾種LayoutManager,例如:LinearLayoutManagerGridLayoutManager等等。git

  仔細想想,咱們使用LayoutManager就像咱們當初初學Android時使用各類基礎控件,咱們處於只會使用的階段,若是後續有一些特殊的要求,系統的實現已經不能知足咱們自身的需求,此時自定義LayoutManager就必須出手了。同時,若是想要自定義LayoutManager,咱們就必須瞭解它相關的原理。因此,學習LayoutManager的源碼是相當重要的。github

  本文參考資料:數組

  1. RecyclerView系列(7)—自定義LayoutManager(上),視覺上定義一個具有上下邊界的RecyclerView.layoutMnager
  2. RecyclerView系列(8)—自定義LayoutManager(下) ,回收複用及優化
  3. LayoutManagerGroup

  介於LayoutManger的特殊性,咱們不可能將LayoutManager及其全部子類的代碼都分析一遍,因此本文的源碼分析重點是,從源碼角度來解釋爲何這樣自定義LayoutManager。自定義LayoutManager要求的門檻相對較高,它不是簡單的照着模板來寫,而是須要了解它內部的原理,這其中包括回收機制(這個咱們在分析RecyclerView的三大流程時已經從LinearLayoutManager內部看到了),滑動機制等等。因此,在自定義LayoutManager時,我默認你們都懂得這些原理,若是還有同窗不懂的話,能夠參考個人文章:緩存

  1. RecyclerView 源碼分析(一) - RecyclerView的三大流程
  2. RecyclerView 源碼分析(二) - RecyclerView的滑動機制
  3. RecyclerView 源碼分析(三) - RecyclerView的緩存機制

  本文打算從以下幾個角度來分析LayoutManager:bash

  1. 知識儲備--相關方法的解釋,這裏的相關方法主要是自定義涉及到的方法
  2. 自定義一個LayoutManager
  3. SnapHelper基本使用、源碼分析和自定義SnapHelper

1. 概述

  在正式分析LayoutManager以前,咱們先來對LayoutManager及其它的相關組件作一個簡單的概述。ide

  咱們都知道LayoutManager就是一個佈局管理器,主要負責RecyclerViewItemView測量和佈局,因此自定義LayoutManager的過程跟自定義View的過程很是的類似。本文打算從一個Demo開始來介紹怎麼自定義一個LayoutManager,效果以下: 源碼分析

  同時在這裏,咱們還介紹了跟 LayoutManager相關的兩個組件-- SnapHelperSmoothScroller。這個其中 SnapHelper主要負責來調整 RecyclerView的滑動距離,好比想要在滑動結束以後, ItemView停留在 RecyclerView正中央,能夠依靠 SnapHelper

2. LayoutManager的相關方法

  咱們在自定義LayoutManager以前,先來看一下LayoutManager的幾個方法。佈局

方法名 做用
generateDefaultLayoutParams 抽象方法,必須實現。這個方法的做用主要是給RecyclerViewItemView生成LayoutParams
onMeasure 用來測量RecyclerView的大小的。一般不用重寫此方法,可是在一種狀況下必須重寫,就是LayouytManager不支持自動測量,這種狀況下RecyclerView不會進行自我測量,會調用LayoutManageronMeasure方法來測量。
onLayoutChildren 此方法的做用是佈局ItemView。此方法就像是ViewGrouponLayout方法,RecyclerView內部的ItemView怎麼佈局,全看這個方法怎麼實現。
canScrollHorizontally 設置該LayoutManagerRecyclerView是否能夠水平滑動。與之對應的還有canScrollVertically,用來設置RecyclerView是否垂直滑動
scrollHorizontallyBy 水平能夠滑動的距離。此方法帶一個dx參數,表示RecyclerView已經產生了dx的滑動距離,此時咱們須要作的是調用相關方法,進行從新佈局。同時此方法的返回值表示水平能夠滑動的距離。與之對應的方法是scrollVerticallyBy

3. 自定義LayoutManager

  簡單的瞭解了自定義LayoutManager的幾個方法,如今我將帶領來實現一個Demo,具體的效果就是上面的gif動圖,咱們來看看怎麼本身實現一個LayoutMananger學習

(1). 重寫generateDefaultLayoutParams方法

  首先,自定義LayoutManager的第一步就是重寫generateDefaultLayoutParams方法,這個方法的做用在上面我已經介紹了,在這裏就不介紹了。一般來講,咱們這樣來實現generateDefaultLayoutParams方法就好了:優化

@Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
    }
複製代碼

  咱們這裏沒有特殊的要求,因此讓每一個ItemView的自適應就好了。

(2). onLayoutChildren方法

  而後,第二步就是重寫onLayoutChildren方法,也是最複雜的一步。在這一步,咱們主要完成兩步:

  1. 定位每一個ItemView的位置,而後佈局。
  2. 適配滑動和縮放的效果。

  咱們先來結合圖片來分析一下這個效果。

  整個效果咱們能夠這麼來考慮, ItemView是從左往右開始佈局,不過咱們得從從右往左計算每一個 ItemView的寬高,由於最右邊的 ItemView寬高是最原始,同時它的left位置也是最容易的計算( RecyclerView的水平空閒空間減去 ItemViewwidth就行。)。

  而後咱們能夠設置一個offset,後面的ItemView根據這個offset來從新定位。咱們經過以前看LinearLayoutManager源碼的經驗,發現LinearLayoutManager計算位置經過一個remainSpace變量來實現的。remainSpace表示當前RecyclerView的剩餘空間,每佈局一個ItemViewremainSpace減去小消耗的距離就OK!

  下面我結合代碼來具體分析:

@Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (state.getItemCount() == 0 || state.isPreLayout()) return;
        removeAndRecycleAllViews(recycler);
        if (!mHasChild) {
            mItemViewHeight = getVerticalSpace();
            mItemViewWidth = (int) (mItemViewHeight / mItemHeightWidthRatio);
            mHasChild = true;
        }
        mItemCount = getItemCount();
        mScrollOffset = makeScrollOffsetWithinRange(mScrollOffset);
        fill(recycler);
    }
複製代碼

  在onLayoutChildren方法裏面,咱們初始化了幾個變量,其中mItemViewHeightmItemViewWidth兩個變量分別表示ItemView的高和寬。其次就是mScrollOffset的初始化:

private int makeScrollOffsetWithinRange(int scrollOffset) {
        return Math.min(Math.max(mItemViewWidth, scrollOffset), mItemCount * mItemViewWidth);
    }
複製代碼

  第一次調用onLayoutChildren方法來初始化mScrollOffset時,mScrollOfffet的值被設置爲mItemCount * mItemViewWidth。這有什麼意義呢?我待會會解釋。

  在onLayoutChidlren方法的最後,調用fill方法。fill方法纔是真正計算每一個ItemView的位置,咱們來看看:

private void fill(RecyclerView.Recycler recycler) {
        // 1.初始化基本變量
        int bottomVisiblePosition = mScrollOffset / mItemViewWidth;
        final int bottomItemVisibleSize = mScrollOffset % mItemViewWidth;
        final float offsetPercent = bottomItemVisibleSize * 1.0f / mItemViewWidth;
        final int space = getHorizontalSpace();
        int remainSpace = space;
        final int defaultOffset = mItemViewWidth / 2;
        final List<ItemViewInfo> itemViewInfos = new ArrayList<>();
        // 2.計算每一個ItemView的位置信息(left和scale)
        for (int i = bottomVisiblePosition - 1, j = 1; i >= 0; i--, j++) {
            double maxOffset = defaultOffset * Math.pow(mScale, j - 1);
            int start = (int) (remainSpace - offsetPercent * maxOffset - mItemViewWidth);
            ItemViewInfo info = new ItemViewInfo(start, (float) (Math.pow(mScale, j - 1) * (1 - offsetPercent * (1 - mScale))));
            itemViewInfos.add(0, info);
            remainSpace -= maxOffset;
            if (remainSpace < 0) {
                info.setLeft((int) (remainSpace + maxOffset - mItemViewWidth));
                info.setScale((float) Math.pow(mScale, j - 1));
                break;
            }
        }
        // 3.添加最右邊ItemView的相關信息
        if (bottomVisiblePosition < mItemCount) {
            final int left = space - bottomItemVisibleSize;
            itemViewInfos.add(new ItemViewInfo(left, 1.0f));
        } else {
            bottomVisiblePosition -= 1;
        }
        // 4.回收其餘位置的View
        final int layoutCount = itemViewInfos.size();
        final int startPosition = bottomVisiblePosition - (layoutCount - 1);
        final int endPosition = bottomVisiblePosition;
        final int childCount = getChildCount();
        for (int i = childCount - 1; i >= 0; i--) {
            final View childView = getChildAt(i);
            final int position = convert2LayoutPosition(i);
            if (position > endPosition || position < startPosition) {
                detachAndScrapView(childView, recycler);
            }
        }
        // 5.先回收再佈局
        detachAndScrapAttachedViews(recycler);
        for (int i = 0; i < layoutCount; i++) {
            fillChild(recycler.getViewForPosition(convert2AdapterPosition(startPosition + i)), itemViewInfos.get(i));
        }
    }
複製代碼

  在分析上面的代碼以前,我先來對幾個變量作一個統一的解釋。

變量名 含義
bottomVisiblePosition 表示此時RecyclerView最右邊能看見的ItemViewposition。例如說,初始狀況下,bottomVisiblePosition就等於ItemCount,固然此時bottomVisiblePosition的結果確定是不對的,後面在使用時會根據狀況來調整。
bottomItemVisibleSize 這個變量沒有特殊意義,主要的用來計算offsetPercent
offsetPercent 滑動的百分比,從1.0f~0.0f變化。
defaultOffset 每一個ItemView偏移的值(默認全部的ItemView都是左對齊)

  而後就是計算每一個ItemView的位置了。這裏須要注意一個問題,就是bottomVisiblePosition == mItemCount的狀況。

  當bottomVisiblePosition == mItemCount時,也是最初的狀態,這種狀況下,第二步就是直接將最右邊的ItemView的位置信息計算出來。

  當bottomVisiblePosition < mItemCoun時(沒有大於的狀況)時,也是在滑動的時,是在第三步時將最右邊的ItemView的位置信息計算出來。

  關於位置信息的計算,這裏就不討論了,都是一些常規的計算邏輯。

  最後就是佈局,調用的是fillChild方法:

private void fillChild(View view, ItemViewInfo itemViewInfo) {
        addView(view);
        measureChildWithExactlySize(view);
        final int top = getPaddingTop();

        layoutDecoratedWithMargins(view, itemViewInfo.getLeft(), top, itemViewInfo.getLeft() + mItemViewWidth, top + mItemViewHeight);
        view.setScaleX(itemViewInfo.getScale());
        view.setScaleY(itemViewInfo.getScale());
    }
複製代碼

  fillChild方法沒有解釋的必要,熟悉自定義View的同窗應該都懂。

  到這裏onLayoutChildren方法算是從新完畢了,這個過程當中,比較難以理解的是位置信息的計算,這個我也不知道怎麼解釋,你們就本身發揮想象力吧。

(3). 水平滑動

  接下來就是讓RecyclerView支持水平滑動。要想支持水平滑動,咱們必須重寫canScrollHorizontally方法和scrollHorizontallyBy方法,咱們來看看:

@Override
    public boolean canScrollHorizontally() {
        return true;
    }

    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int pendingScrollOffset = mScrollOffset + dx;
        mScrollOffset = makeScrollOffsetWithinRange(pendingScrollOffset);
        fill(recycler);
        return mScrollOffset - pendingScrollOffset + dx;
    }
複製代碼

  這個過程當中,須要特別注意的是scrollHorizontallyBy方法,咱們不能直接讓mScrollOffset加上dx,由於mScrollOffset的範圍在[mItemViewWidth,mItemCount * mItemViewWidth],因此在每次滑動以後須要調整,得再一次調用makeScrollOffsetWithinRange方法。

(3). 滑動以後最右邊的ItemView都能完整顯示

  這個需求就很是的簡單,自我實現一個SnaHelper,而後這樣使用就OK了:

private final SnapHelper mSnapHelper = new CustomSnapHelper();
    @Override
    public void onAttachedToWindow(RecyclerView view) {
        super.onAttachedToWindow(view);
        mSnapHelper.attachToRecyclerView(view);
    }
複製代碼

  這裏面具體的含義這裏先不解釋,待會在分析SnaHelper時會詳細的解釋。

(5). 源碼

  整個LayoutManager的自定義過程就OK了,具體的效果就是上面的動圖效果。

  還有不懂的同窗能夠個人github去下載源碼:LayoutManagerDemo。特別感謝:LayoutManagerGroup,本文自定義的LayoutManager大部分思路和源碼都來至於它。

4. SnapHelper

  SnaHelper的存在對於RecyclerView來講,可謂是如虎添翼。SnaHelper可見幫助咱們實現一些特殊的效果,好比說,咱們可使用RecyclerViewSnapHelper去實現ViewPager的效果。

  一般來講,咱們在平常開發中,使用RecyclerView不多遇到的SnapHelper,不過,若是你想要自定義LayoutManager來實現一些特殊效果,很大的可能性會遇到SnapHelper。那麼SnapHelper究竟是什麼呢?是怎麼使用的呢?它的實現原理又是什麼呢?這是本文須要解答的三個問題。

  簡單來講,SnapHelper就是一個Helper類,只是它的內部有兩個監聽接口:OnFlingListenerOnScrollListener,分別用來監聽RecyclerView的scroll事件和fling事件。

  而SnapHelper的使用也是很是的簡單,就是在LayoutManageronAttachedToWindow方法調用SnapHelperattachToRecyclerView方法便可。咱們就從attachToRecyclerView方法爲入口來分析SnapHelper的源碼。

(1). SnapHelper的源碼分析

  SnapHelper的原理其實是很是的簡單,你們不要懼怕。咱們在分析SnapHelper源碼以前,先來了解SnapHelper幾個比較重要的方法:

方法名 返回類型 含義
calculateDistanceToFinalSnap int[] 計算RecyclerView最終滑動的距離。返回的是一個長度爲2的數組,其中0位置表示水平滑動的滑動距離,1位置表示垂直滑動的距離。
findTargetSnapPosition int 這個方法表示fling操做最終能滑動到I的temView的position。這個position稱爲targetSnapPosition,位置上對應的View就是targetSnapView。若是找不到position,就返回RecyclerView.NO_POSITION
findSnapView View 最終滑動位置對應的ItemView

  在這裏,咱們必須區分一下findTargetSnapPosition方法和calculateDistanceToFinalSnapfindSnapView方法的區別。

  1. findTargetSnapPosition:此方法表示fling滑動能滑到的位置。
  2. calculateDistanceToFinalSnap和findSnapView:這兩個方法表示正常滑動的能到達位置,其中calculateDistanceToFinalSnap表示距離,這個過程涉及到由於對齊操做而進行的距離從新調整;findSnapView方法表示正常滑動能到達的位置對應的ItemView

  因此,咱們在自定義SnapHelper時,爲了簡單起見,不能夠處理fling操做,也就是findTargetSnapPosition返回爲RecyclerView.NO_POSITION,而後讓RecyclerView本身進行fling ,等待滑動結束以後,會回調咱們的calculateDistanceToFinalSnapfindSnapView來進行位置對齊。這樣作的好處就是,咱們不用既考慮fling又考慮普通滑動。

A.attachToRecyclerView方法

  準備的差很少了,接下來咱們正式分析SnapHelper的源碼。咱們來看看attachToRecyclerView方法:

public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
            throws IllegalStateException {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            setupCallbacks();
            mGravityScroller = new Scroller(mRecyclerView.getContext(),
                    new DecelerateInterpolator());
            snapToTargetExistingView();
        }
    }
複製代碼

  attachToRecyclerView很是的簡單,就是設置給RecyclerView設置了兩個監聽接口:

private void setupCallbacks() throws IllegalStateException {
        if (mRecyclerView.getOnFlingListener() != null) {
            throw new IllegalStateException("An instance of OnFlingListener already set.");
        }
        mRecyclerView.addOnScrollListener(mScrollListener);
        mRecyclerView.setOnFlingListener(this);
    }
複製代碼

  而後RecyclerView開心的滑動,就會回調到咱們的兩個監聽事件裏面來。

B.OnScrollListener

  咱們先來看看OnScrollListener接口的實現,看看它作了哪些事情:

private final RecyclerView.OnScrollListener mScrollListener =
            new RecyclerView.OnScrollListener() {
                boolean mScrolled = false;

                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
                        mScrolled = false;
                        snapToTargetExistingView();
                    }
                }

                @Override
                public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                    if (dx != 0 || dy != 0) {
                        mScrolled = true;
                    }
                }
            };
複製代碼

  咱們發現,當RecyclerView滑動結束以後,就會調用snapToTargetExistingView方法。那snapToTargetExistingView方法是幹嗎的呢?其實就是保證對齊的。咱們來看看:

void snapToTargetExistingView() {
        if (mRecyclerView == null) {
            return;
        }
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return;
        }
        View snapView = findSnapView(layoutManager);
        if (snapView == null) {
            return;
        }
        int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView);
        if (snapDistance[0] != 0 || snapDistance[1] != 0) {
            mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]);
        }
    }
複製代碼

  咱們發現,在這裏先是調用了findSnapView方法找到滑動的最終ItemView,而後根據找到的SnapView,調用calculateDistanceToFinalSnap方法來計算滑動的距離,最後調用相關方法來進行對齊。整個過程就是這麼的簡單。

C. OnFlingListener

  SnapHelper內部自己沒有一個OnFingListener接口對象,而是自身實現了OnFingListener,因此當RecyclerView在fling時,會回調此onFling方法。咱們來看看:

@Override
    public boolean onFling(int velocityX, int velocityY) {
        LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) {
            return false;
        }
        RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
        if (adapter == null) {
            return false;
        }
        int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
        return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity)
                && snapFromFling(layoutManager, velocityX, velocityY);
    }
複製代碼

  首先,咱們要明白一個東西,若是RecyclerView有一個OnFlingListener處理fling事件的話,那麼RecyclerView就不會再處理fling事件。

  因此SnapHelper是否處理fling事件,還須要看它的snapFromFling方法。咱們來看看:

private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX,
            int velocityY) {
        if (!(layoutManager instanceof ScrollVectorProvider)) {
            return false;
        }

        SmoothScroller smoothScroller = createScroller(layoutManager);
        if (smoothScroller == null) {
            return false;
        }

        int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY);
        if (targetPosition == RecyclerView.NO_POSITION) {
            return false;
        }

        smoothScroller.setTargetPosition(targetPosition);
        layoutManager.startSmoothScroll(smoothScroller);
        return true;
    }
複製代碼

  在上面的代碼中,咱們發現,findTargetSnapPosition若是返回爲RecyclerView.NO_POSITION,那麼SnapHelper就不會處理fling事件。而若是SnapHelper要處理fling事件的話,會經過LayoutManagerstartSmoothScroll方法。這裏面的原理實際上仍是調用到RecyclerViewViewFlinger裏面去了。

  整個SnapHelper的原理就是這樣,很是的簡單,接下來咱們結合實際來看看怎麼自定義一個SnapHelper

(2).自定義SnapHelper

  一般來講,咱們自定義SnapHelper,實現三個抽象方法就已經差很少,分別是calculateDistanceToFinalSnap方法、findTargetSnapPosition方法和findSnapView方法就已經夠了。我麼來看看咱們本身實現的CustomSnapHelper:

public class CustomSnapHelper extends SnapHelper {

    @Override
    public int[] calculateDistanceToFinalSnap(
            @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {

        if (layoutManager instanceof CustomLayoutManger) {
            int[] out = new int[2];
            if (layoutManager.canScrollHorizontally()) {
                out[0] = ((CustomLayoutManger) layoutManager).calculateDistanceToPosition(
                        layoutManager.getPosition(targetView));
                out[1] = 0;
            } else {
                out[0] = 0;
                out[1] = ((CustomLayoutManger) layoutManager).calculateDistanceToPosition(
                        layoutManager.getPosition(targetView));
            }
            return out;
        }
        return null;
    }

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
                                      int velocityY) {
        return RecyclerView.NO_POSITION;
    }

    @Override
    public View findSnapView(RecyclerView.LayoutManager layoutManager) {
        if (layoutManager instanceof CustomLayoutManger) {
            int pos = ((CustomLayoutManger) layoutManager).getFixedScrollPosition();
            if (pos != RecyclerView.NO_POSITION) {
                return layoutManager.findViewByPosition(pos);
            }
        }
        return null;
    }
}
複製代碼

  方法的具體含義我這裏就再也不解釋了,你們能夠個人Demo項目和上面對三個方法的解釋來進行理解,總之來講,SnapHelper仍是比較簡單的。

5. 總結

  到這裏,咱們對LayoutManager相關分析就差很少,在最後,我作一個小小的總結。

  1. 自定義LayoutManager須要注意四點:1.重寫generateDefaultLayoutParams方法;2.重寫onLayoutChildren方法,對ItemView進行佈局;3. 處理滑動,例如水平滑動須要重寫canScrollHorizontallyscrollHorizontallyBy;4. 若是須要處理對齊問題,可使用SnapHelper
  2. 自定義SnapHelper咱們只須要重寫它的三個抽象方法便可,分別是:calculateDistanceToFinalSnapfindTargetSnapPositionfindSnapView。須要注意的是,爲了簡單起見,咱們能夠直接在findTargetSnapPosition內部返回RecyclerView.NO_POSITION,讓RecyclerView來幫助咱們處理fling事件。

  若是不出意外的話,接下來我將分析ItemAnimator

相關文章
相關標籤/搜索