對於使用ReccyclerView
的咱們來講,LayoutManager
早已很是熟悉。但是,有沒有想過咱們所說的熟悉是哪一種熟悉?對的,就是會使用而已,這其中包括谷歌爸爸幫咱們實現的幾種LayoutManager
,例如:LinearLayoutManager
,GridLayoutManager
等等。git
仔細想想,咱們使用LayoutManager
就像咱們當初初學Android時使用各類基礎控件,咱們處於只會使用的階段,若是後續有一些特殊的要求,系統的實現已經不能知足咱們自身的需求,此時自定義LayoutManager
就必須出手了。同時,若是想要自定義LayoutManager
,咱們就必須瞭解它相關的原理。因此,學習LayoutManager
的源碼是相當重要的。github
本文參考資料:數組
介於LayoutManger
的特殊性,咱們不可能將LayoutManager
及其全部子類的代碼都分析一遍,因此本文的源碼分析重點是,從源碼角度來解釋爲何這樣自定義LayoutManager
。自定義LayoutManager
要求的門檻相對較高,它不是簡單的照着模板來寫,而是須要了解它內部的原理,這其中包括回收機制(這個咱們在分析RecyclerView
的三大流程時已經從LinearLayoutManager
內部看到了),滑動機制等等。因此,在自定義LayoutManager
時,我默認你們都懂得這些原理,若是還有同窗不懂的話,能夠參考個人文章:緩存
本文打算從以下幾個角度來分析LayoutManager
:bash
- 知識儲備--相關方法的解釋,這裏的相關方法主要是自定義涉及到的方法
- 自定義一個
LayoutManager
SnapHelper
基本使用、源碼分析和自定義SnapHelper
在正式分析LayoutManager
以前,咱們先來對LayoutManager
及其它的相關組件作一個簡單的概述。ide
咱們都知道LayoutManager
就是一個佈局管理器,主要負責RecyclerView
的ItemView
測量和佈局,因此自定義LayoutManager
的過程跟自定義View
的過程很是的類似。本文打算從一個Demo開始來介紹怎麼自定義一個LayoutManager
,效果以下: 源碼分析
LayoutManager
相關的兩個組件--
SnapHelper
和
SmoothScroller
。這個其中
SnapHelper
主要負責來調整
RecyclerView
的滑動距離,好比想要在滑動結束以後,
ItemView
停留在
RecyclerView
正中央,能夠依靠
SnapHelper
。
咱們在自定義LayoutManager
以前,先來看一下LayoutManager
的幾個方法。佈局
方法名 | 做用 |
---|---|
generateDefaultLayoutParams | 抽象方法,必須實現。這個方法的做用主要是給RecyclerView 的ItemView 生成LayoutParams |
onMeasure | 用來測量RecyclerView 的大小的。一般不用重寫此方法,可是在一種狀況下必須重寫,就是LayouytManager 不支持自動測量,這種狀況下RecyclerView 不會進行自我測量,會調用LayoutManager 的onMeasure 方法來測量。 |
onLayoutChildren | 此方法的做用是佈局ItemView 。此方法就像是ViewGroup 的onLayout 方法,RecyclerView 內部的ItemView 怎麼佈局,全看這個方法怎麼實現。 |
canScrollHorizontally | 設置該LayoutManager 的RecyclerView 是否能夠水平滑動。與之對應的還有canScrollVertically ,用來設置RecyclerView 是否垂直滑動 |
scrollHorizontallyBy | 水平能夠滑動的距離。此方法帶一個dx參數,表示RecyclerView 已經產生了dx 的滑動距離,此時咱們須要作的是調用相關方法,進行從新佈局。同時此方法的返回值表示水平能夠滑動的距離。與之對應的方法是scrollVerticallyBy 。 |
簡單的瞭解了自定義LayoutManager
的幾個方法,如今我將帶領來實現一個Demo,具體的效果就是上面的gif動圖,咱們來看看怎麼本身實現一個LayoutMananger
。學習
首先,自定義LayoutManager
的第一步就是重寫generateDefaultLayoutParams
方法,這個方法的做用在上面我已經介紹了,在這裏就不介紹了。一般來講,咱們這樣來實現generateDefaultLayoutParams
方法就好了:優化
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT);
}
複製代碼
咱們這裏沒有特殊的要求,因此讓每一個ItemView
的自適應就好了。
而後,第二步就是重寫onLayoutChildren
方法,也是最複雜的一步。在這一步,咱們主要完成兩步:
- 定位每一個ItemView的位置,而後佈局。
- 適配滑動和縮放的效果。
咱們先來結合圖片來分析一下這個效果。
ItemView
是從左往右開始佈局,不過咱們得從從右往左計算每一個
ItemView
的寬高,由於最右邊的
ItemView
寬高是最原始,同時它的left位置也是最容易的計算(
RecyclerView
的水平空閒空間減去
ItemView
的
width
就行。)。
而後咱們能夠設置一個offset
,後面的ItemView
根據這個offset來從新定位。咱們經過以前看LinearLayoutManager
源碼的經驗,發現LinearLayoutManager
計算位置經過一個remainSpace
變量來實現的。remainSpace
表示當前RecyclerView
的剩餘空間,每佈局一個ItemView
,remainSpace
減去小消耗的距離就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
方法裏面,咱們初始化了幾個變量,其中mItemViewHeight
和mItemViewWidth
兩個變量分別表示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 最右邊能看見的ItemView 的position 。例如說,初始狀況下,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
方法算是從新完畢了,這個過程當中,比較難以理解的是位置信息的計算,這個我也不知道怎麼解釋,你們就本身發揮想象力吧。
接下來就是讓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
方法。
這個需求就很是的簡單,自我實現一個SnaHelper
,而後這樣使用就OK了:
private final SnapHelper mSnapHelper = new CustomSnapHelper();
@Override
public void onAttachedToWindow(RecyclerView view) {
super.onAttachedToWindow(view);
mSnapHelper.attachToRecyclerView(view);
}
複製代碼
這裏面具體的含義這裏先不解釋,待會在分析SnaHelper
時會詳細的解釋。
整個LayoutManager
的自定義過程就OK了,具體的效果就是上面的動圖效果。
還有不懂的同窗能夠個人github去下載源碼:LayoutManagerDemo。特別感謝:LayoutManagerGroup,本文自定義的LayoutManager大部分思路和源碼都來至於它。
SnaHelper
的存在對於RecyclerView
來講,可謂是如虎添翼。SnaHelper
可見幫助咱們實現一些特殊的效果,好比說,咱們可使用RecyclerView
和SnapHelper
去實現ViewPager
的效果。
一般來講,咱們在平常開發中,使用RecyclerView
不多遇到的SnapHelper
,不過,若是你想要自定義LayoutManager
來實現一些特殊效果,很大的可能性會遇到SnapHelper
。那麼SnapHelper
究竟是什麼呢?是怎麼使用的呢?它的實現原理又是什麼呢?這是本文須要解答的三個問題。
簡單來講,SnapHelper
就是一個Helper類,只是它的內部有兩個監聽接口:OnFlingListener
和OnScrollListener
,分別用來監聽RecyclerView
的scroll事件和fling事件。
而SnapHelper
的使用也是很是的簡單,就是在LayoutManager
的onAttachedToWindow
方法調用SnapHelper
的attachToRecyclerView
方法便可。咱們就從attachToRecyclerView
方法爲入口來分析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
方法和calculateDistanceToFinalSnap
、findSnapView
方法的區別。
- findTargetSnapPosition:此方法表示fling滑動能滑到的位置。
- calculateDistanceToFinalSnap和findSnapView:這兩個方法表示正常滑動的能到達位置,其中
calculateDistanceToFinalSnap
表示距離,這個過程涉及到由於對齊操做而進行的距離從新調整;findSnapView
方法表示正常滑動能到達的位置對應的ItemView
。
因此,咱們在自定義SnapHelper
時,爲了簡單起見,不能夠處理fling操做,也就是findTargetSnapPosition
返回爲RecyclerView.NO_POSITION
,而後讓RecyclerView
本身進行fling
,等待滑動結束以後,會回調咱們的calculateDistanceToFinalSnap
和findSnapView
來進行位置對齊。這樣作的好處就是,咱們不用既考慮fling又考慮普通滑動。
準備的差很少了,接下來咱們正式分析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
開心的滑動,就會回調到咱們的兩個監聽事件裏面來。
咱們先來看看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
方法來計算滑動的距離,最後調用相關方法來進行對齊。整個過程就是這麼的簡單。
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事件的話,會經過LayoutManager
的startSmoothScroll
方法。這裏面的原理實際上仍是調用到RecyclerView
的ViewFlinger
裏面去了。
整個SnapHelper
的原理就是這樣,很是的簡單,接下來咱們結合實際來看看怎麼自定義一個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
仍是比較簡單的。
到這裏,咱們對LayoutManager
相關分析就差很少,在最後,我作一個小小的總結。
- 自定義LayoutManager須要注意四點:1.重寫
generateDefaultLayoutParams
方法;2.重寫onLayoutChildren
方法,對ItemView
進行佈局;3. 處理滑動,例如水平滑動須要重寫canScrollHorizontally
和scrollHorizontallyBy
;4. 若是須要處理對齊問題,可使用SnapHelper
。- 自定義
SnapHelper
咱們只須要重寫它的三個抽象方法便可,分別是:calculateDistanceToFinalSnap
、findTargetSnapPosition
和findSnapView
。須要注意的是,爲了簡單起見,咱們能夠直接在findTargetSnapPosition
內部返回RecyclerView.NO_POSITION
,讓RecyclerView
來幫助咱們處理fling事件。
若是不出意外的話,接下來我將分析ItemAnimator
。