步步前行(RecyclerView拆分解析)-ViewGroup篇
不喜勿噴,有錯請留言(以下源碼均來自recyclerView-27.1.1版本)
對於android開發而言,recyclerView是最熟悉常用的組件了,在很多層面上,常常替代ListView,往上分析RecyclerView的代碼文章更是數不勝數,不過這一篇我想換一個角度,從拆分RecyclerView,讓Recyclerview迴歸ViewGroup的角度來理解RecyclerView,那我們如何去拆分呢?我決定以一個模擬的方式,從RecyclerView上拆分下來的功能模塊在模擬類上使用,從而理解其功能
組成
首先相信大家或多或少都使用過RecyclerView,對於最最基本的使用我相信點進來的都會,如圖
我們必須要創建一個RecyclerView,然後爲這個RecyclerView準備兩樣東西一個是LayoutManager,一個是Adapter,也就是擺放佈局和數據源
LayoutManager決定了我們RecyclerView如何展示橫向還是豎向等,Adapter決定了我們RecyclerView條目個數和條目樣式,至此一個RecyclerView就按照我們的想法呈現在我們面前了。
接下來我們先粗略的看看RecyclerView的組成:
一目瞭然,RecyclerView本身繼承的viewGroup,所以RecyclerView形容成ViewGroup也不爲過
既然是viewGroup,那麼它自身必定重寫onLayout和onMeasure
onMeasure
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
stopInterceptRequestLayout(false);
} else if (mState.mRunPredictiveAnimations) {
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
startInterceptRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
stopInterceptRequestLayout(false);
mState.mInPreLayout = false; // clear
}
}
首先layoutManager爲null的情況下會執行defaultOnMeasure方法,我們看看defaultOnMeasure方法
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is better
// than creating another method since this is internal.
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
**************************************************************
從該方法中我們可以看出其直接調用了setMeasuredDimension設置了寬高,併爲對子view做任何的測量
接着對LayoutManager的isAutoMeasureEnabled的不同狀態,做了兩種情況,我們先看看isAutoMeasureEnabled,由於是layoutManager我們不能以RecyclerView的實現爲準,我們參考LinearlayoutManager,對於LinearLayoutManager,其是這麼實現的
@Override
public boolean isAutoMeasureEnabled() {
return true;
}
******************************************
在LinearLayoutManager中,竟恆定給了true
我們繼續順着onMeasure的true情況看看
**********onMeasure爲true的部分代碼*********
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
*******************************************
在這裏先獲取mode,若爲MeasureSpec.EXACTLY即精確值時,就直接return掉不再往下執行了
而接下來的代碼:
若是始態,執行dispatchLayoutStep1
********************************
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
**************************************************************
執行完dispatchLayoutStep1和dispatchLayoutStep2擺放後,這時候我們就可以獲取寬高了,下面就是獲取寬高,並設置給LayoutManager,避免有不精確的寬高時,就重新測量
**************************************************************
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
至此onMeasure部分的就執行完畢了,接下來就是開始onLayout的部分了。
onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
**************************************************************
TraceCompat是support-v4提供的統計一下trace事件給系統
*************************************************************
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();//這裏是我們分析的重點
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
onLayout的dispatchLayout分成了三部分,根據不同的狀態執行了三步:dispatchLayoutStep1,dispatchLayoutStep2和dispatchLayoutStep3
void dispatchLayout() {
******************************************************
此處省略了對layoutManger和adapter進行判null處理...
******************************************************
mState.mIsMeasuring = false;
**************************************************************
mState是對我們循環過程狀態的標記,默認是STEP_START,避免重複執行和不必要的計算,執行dispatchLayoutStep1和dispatchLayoutStep2
* STEP_START -> dispatchLayoutStep1()
* STEP_LAYOUT -> dispatchLayoutStep2()
* STEP_ANIMATIONS -> dispatchLayoutStep2(), dispatchLayoutStep3()
**************************************************************
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {//這個判斷條件,我們很容易可看出當adapter或layout進行變化的時候,就進行dispatchLayoutStep2
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
*************************************
最後每次都執行dispatchLayoutStep3
*************************************
dispatchLayoutStep3();
}
那我們先從dispatchLayoutStep1開始看,其主要作用是用於動畫等視圖信息,我們大致分析一下簡單理解
private void dispatchLayoutStep1() {
mState.assertLayoutStep(State.STEP_START);
*****************************************************
計算滑動的距離並存儲進state
*****************************************************
fillRemainingScrollValues(mState);
mState.mIsMeasuring = false;
*****************************************************
startInterceptRequestLayout與stopInterceptRequestLayout聯動使用
在子view可能觸發requestLayout前和後調用,主要用於截斷requestLayout重新佈局
覺得不太明白的可以看RecyclerView的requestLayout方法再輔助viewGroup的整個requestLayout傳遞可瞭解這一步
**************************************************
startInterceptRequestLayout();
還原子view的狀態
mViewInfoStore.clear();
做layout或scroll的計數器
onEnterLayoutOrScroll();
使用適配器更新並計算要運行的動畫類型(Add/REMOVE/UPDATE/MOVE)並存儲到state中
processAdapterUpdatesAndSetAnimationFlags();//不做深入分析
存儲焦點view的信息
saveFocusInfo();
mState.mTrackOldChangeHolders = mState.mRunSimpleAnimations && mItemsChanged;
mItemsAddedOrRemoved = mItemsChanged = false;
mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
根據所有子view的layoutPosition計算動畫最小位置和最大位置
findMinMaxChildLayoutPositions(mMinMaxLayoutPositions);
根據processAdapterUpdatesAndSetAnimationFlags計算的動畫類型,去處理做動畫
if (mState.mRunSimpleAnimations) {
...
}
if (mState.mRunPredictiveAnimations) {
...
} else {
clearOldPositions();
}
維護計數器
onExitLayoutOrScroll();
還原requestLayout之行順序
stopInterceptRequestLayout(false);
維護mLayoutStep 準備進行layout
mState.mLayoutStep = State.STEP_LAYOUT;
}
這第一步dispatchLayoutStep1到這裏就結束了
下面我們進入dispatchLayoutStep2看看其是什麼樣的作用,從其官方註釋我們可以瞭解,這個方法是整個viewGroup的重中之重,是對實圖的最終形態佈局
private void dispatchLayoutStep2() {
老樣子 與第一步很一致呀 不解釋這裏了
startInterceptRequestLayout();
onEnterLayoutOrScroll();
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
*****************************************************
進入layout詳細,進入到了layoutManager的onLayoutChildren方法中,至此我們如何的擺放就通過這裏傳遞到了LayoutManager身上,交由對方決定(此處會在拆分解析-LayoutManager篇做詳細解釋)
*****************************************************
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
mPendingSavedState = null;
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}
其實到這裏我們就已經完成了,佈局的展示了,最後的dispatchLayoutStep3,其實是對dispatchLayoutStep1的動畫進行清理等操作,這裏就不做詳細的解釋
最後可能有小夥伴會發現還有onDraw操作,我們就簡單分析下onDraw,其實對於Recyclerview而言onDraw實現是最簡單的,基本上都是圍繞ItemDecoration去寫的
onDraw
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
還有一個重寫了View的draw方法
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
...
}
細心的朋友會發現,其調用的位置不同,一個調用的ItemDecoration的onDraw方法,一個調用的ItemDecoration的onDrawOver方法
onDraw是在畫布上draw一些我們想要的東西,其繪製的內容在我們的視圖之前,也就是說視圖壓在Decoration之上,
而onDrawOver是與onDraw一致,但其在視圖之後,也就是說它壓在我們的視圖之上畫的
如果不理解這兩個方法,我用兩張圖來輔助大家理解下:
從上圖我們可看到,代表onDraw的視圖被RecyclerView視圖所遮擋,而onDrawOver的視圖卻遮擋着RecyclerView視圖
至此RecyclerView的onMeasure和onLayout就分析結束了,