值得深入學習的控制元件-RecyclerView(原始碼解析篇)
作者:錦小白
為什麼要寫這篇原始碼解析呢?
我一直在說RecyclerView是一個值得深入學習,甚至可以說是一門具有藝術性的控制元件。那到底哪裡值得我們花時間去深入學習呢。沒錯了,就是原始碼的設計。但是看原始碼其實是一件不簡單的事情,就拿RecyclerView的原始碼來說,開啟原始碼一看,往下拉啊拉啊,我擦,怎麼還沒到頭,汗....居然有12k+行。看到這裡恐怕會嚇一跳,就這麼一個看似簡單的控制元件就這麼多行原始碼,這讓我從何看起,一股畏懼感油然而生。
其實不需要害怕,我們不需要一開始就想完全弄懂它每一步怎麼實現的,這樣反而會造成只見森林不見樹木的感覺。我們就把原始碼就當成一片森林來說吧。首先我們只需要先抓住一條路徑去看,也就是帶著一個問題去看,這樣就能夠把這條路徑上的樹都看明白了。就不會有隻見森林不見樹,一臉茫然了。當然我們大多數情況肯定是不滿足於此一條路徑,想完全看明白它是怎麼實現的,那就繼續另開路徑(再帶著另外一個問題),繼續看這條路上的樹。當你把每條路都走差不多了,再回頭來看,就會發現你既見到了森林又見到了一顆顆清晰樹木,猶如醍醐灌頂、豁然開朗。
說著很簡單,但是不得不說看原始碼的過程還是有點小痛苦的。不過,不用慌,看完之後你所獲得那種充實感和滿足感會遠遠大於過程中的痛苦感。畢竟這是一個充滿藝術感的控制元件嘛,值得我們去欣賞和學習。
那麼開始放正片了......
一、開闢一條路徑
從使用RecyclerView的時候,它的一個功能就讓我感覺很這個控制元件不簡單,不知道你和我想的是不是一樣。那是什麼功能呢?我們只需改變一行程式碼就可以直接設定它的ItemView為水平佈局、垂直佈局、表格佈局以及瀑布流佈局。這是ListView所不能做到的。用起來簡單,其背後肯定有故事啊。那我們就以這條路為核心來看這片森林了。
二、開始尋路
從哪裡開始看呢?
1.我們先從setAdapter()看起,這個方法我們比較熟悉,在Activity中這是我們直接接觸的方法。
/**
*Replaces the current adapter with the new one and triggers listeners.
*/
public void setAdapter(Adapter adapter){
.....
//用一個新的設配器和觸發器來替代目前正在使的
setAdapterInternal(adapter,false,true);
//請求佈局,直接呼叫View類的請求佈局方法
requestLayout();
}
setAdapter裡面主要做了兩件事:
首先呼叫setAdapterInternal方法,目的是用一個新的設配器和觸發器來替代目前正在使用的。
我們深入進去看看它做了什麼?
對於熟悉了觀察者設計模式的,可以從下面的程式碼看出來,其實裡面有個操作是:
登出觀察者(之前的設配器)和註冊觀察者(新的設配器)操作。簡單的理解一下就是設配器觀察者會監測一些物件的狀態,當這些物件狀態改變,它可以透過這種設計模式低耦合的做出相應的改變。最後呼叫markKnownViewsInvalid方法重新整理一下檢視。
如果你想深入瞭解觀察者設計模式的可以看一下這篇文章
傳送門:
{
Adapter mAdapter;
......
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver); //登出觀察者
mAdapter.onDetachedFromRecyclerView(this); //Called by RecyclerView when it stops observing this Adapter.
}
......
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
adapter.registerAdapterDataObserver(mObserver); //註冊觀察者
adapter.onAttachedToRecyclerView(this);
}
......
//重新整理檢視
markKnownViewsInvalid();
}
之後呼叫了 requestLayout方法請求重新佈局。這個方法很關鍵,和我們的這次選的路是相通的。
@Override
public void requestLayout() {
if (mEatRequestLayout == 0 && !mLayoutFrozen) {
super.requestLayout();
} else {
mLayoutRequestEaten = true;
}
}
這麼關鍵的方法程式碼卻這麼少?而且好像只做了一個操作?沒錯,表面上只呼叫了父類View的requestLayout方法。其實透過父類的這個方法之後會呼叫它的onLayout方法,這個名字熟悉自定義View的童鞋都知道了。但我們看父類View的onLayout方法其實是個空方法。也就是說最終需要由它的子類來重寫,也即RecyclerVie呼叫自身的onLayout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
onLayout又呼叫了dispatchLayout方法,來分發layout
void dispatchLayout() {
......
if (mState.mLayoutStep == State.STEP_START) {
//分發第一步
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
//分發第二步
dispatchLayoutStep2();
}
......
//分發第三步
dispatchLayoutStep3();
......
}
它把這個分發的過程分為了三步走
step1:做一下準備工作:決定哪一個動畫被執行,儲存一些目前view的相關資訊
private void dispatchLayoutStep1() {
......
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i
step2:找到實際的view和最終的狀態後執行layout。
private void dispatchLayoutStep2() {
eatRequestLayout();
onEnterLayoutOrScroll();
......
mState.mInPreLayout = false;
// Step 2: 執行layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = false;
....
resumeRequestLayout(false);
}
這裡面有個方法很關鍵了,就是下面這個onLayoutChildren,這個為什麼關鍵呢,先提一下這個,待會要詳細說的。
mLayout.onLayoutChildren(mRecycler, mState);
step3:做一些分發的收尾工作了,儲存動畫和一些其他的資訊。和我們不同路,就不看它了。
看了這麼多先喝一杯92年的肥宅快樂水壓壓驚吧~~,順便看張圖小結一下上面的過程
三、尋得果樹
之前說過RecyclerView和ListView最大的不同就是在它們的佈局實現上。在ListView中佈局是透過自身的layoutChildren方法實現的,但對於RecyclerView來說就不是了,那是誰來實現了呢?
這就要從剛才結束的onLayoutChildren方法說起了,它不是RecyclerView的類直接方法,它是RecyclerView的內部類LayoutManager的方法,顧名思義,就是佈局管理者了。我們的RecyclerView佈局就透過這個佈局管理者來做了,把這樣一個很重要的職責就交給它了。從而實現某種程度上的低耦合。
那我們繼續走,它是怎麼執行這一職責的。
但是點進去看onLayoutChildren方法,發現只有一行程式碼,而且還是列印的日誌:必須重寫這個方法。
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
那麼既然要重寫必須要尋找一個子類,所以這裡我就找了一個子類LinearLayoutManager類,也是我們最常用的一種線性佈局來看。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
......
int startOffset;
int endOffset;
final int firstLayoutDirection;
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
......
if (mAnchorInfo.mLayoutFromEnd) {
// 底部向頂部的填充
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
//填充
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
// 頂部向底部的填充
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
//填充
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
......
}
} else {
......
}
......
}
這個方法主要就是透過一個佈局演算法,實現itemView從頂部到底部或者底部到頂部的填充,並建立一個佈局的狀態。接下來看一下fill方法是怎麼進行填充的。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
......
//1.計算RecyclerView可用的佈局寬或高
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//2.迭代佈局item View
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//3.佈局item view
layoutChunk(recycler, state, layoutState, layoutChunkResult);
//4.計算佈局偏移量
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
//5.計算剩餘的可用空間
remainingSpace -= layoutChunkResult.mConsumed;
}
......
}
return start - layoutState.mAvailable;
}
fill方法總的來說用了5步實現了itemVIew的填充:
(1)計算RecyclerView可用的佈局寬或高
(2)迭代佈局item View
(3)佈局itemview
(4)計算佈局偏移量
(5)計算剩餘的可用空間
fill方法又會迴圈的呼叫layoutChunk來進行itemView的佈局,下面先看看layoutChunk的實現
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//1.獲取itemview
View view = layoutState.next(recycler);
......
//2.獲取itemview的佈局引數
LayoutParams params = (LayoutParams) view.getLayoutParams();
//3.測量Item View
measureChildWithMargins(view, 0, 0);
//4.計算該itemview消耗的寬和高
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
//5.按照水平或豎直方向佈局來計算itemview的上下左右座標
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
......
}
6.計算itemview的邊界比如下劃線和margin,從而確定itemview準確的位實現最終的佈局
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
result.mFocusable = view.hasFocusable();
}
在layoutChunk中首先從layoutState獲取此時的itemview,然後根據獲得的這個itemview獲取它的佈局引數和尺寸資訊,並且判斷佈局方式(橫向或者縱向),以此計算出itemview的上下左右座標。最後呼叫layoutDecoratedWithMargins方法完成佈局。
這樣一看就對整個過程有了個清晰的認識了吧,有沒有感覺設計的很優雅。
四、貫穿佈局的一條線
到這裡已經算走完我們之前準備走的一條路了。但從開始到這裡始終忽略了一個東西沒有說,那就在佈局過程的大多方法中的引數都有一個Recycler物件。這個Recycler是什麼呢?
在使用RecyclerView的過程中,我們都知道Adapter被快取的單位不再是普通的itemview了,而是一個ViewHolder。這是和listview的一個很大的不同。
public final class Recycler {
final ArrayList mAttachedScrap = new ArrayList();
ArrayList mChangedScrap = null;
final ArrayList mCachedViews = new ArrayList();
private final List
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
...
}
......
在Recycler類的開始就看到mAttachedScrap、mChangedScrap、mCachedViews、 mUnmodifiableAttachedScrap這幾個ViewHolder的列表物件,它們就是用來快取ViewHolder的。
具體是怎麼實現的這裡就不做詳細的解釋了。因為這裡一說又會牽涉到其他的點,子子孫孫無窮盡也,畢竟這是一個有藝術感的控制元件,不能指望一篇文章把它說透哈。
到這裡我們就結束了我們對RecyclerView的的原始碼分析了。相信你看完會有所收穫。
閱讀更多
相信自己,沒有做不到的,只有想不到的
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1020/viewspace-2812981/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- RecyclerView原始碼解析View原始碼
- RecyclerView用法和原始碼深度解析View原始碼
- RecyclerView 原始碼深入解析——繪製流程、快取機制、動畫等View原始碼快取動畫
- RecyclerView 原始碼分析(一) —— 繪製流程解析View原始碼
- ImageLoader深入原始碼學習探究原始碼
- Express原始碼學習-路由篇Express原始碼路由
- 教你玩轉 Android RecyclerView:深入解析 RecyclerView ItemDecoration類AndroidView
- 深入Mybatis原始碼——配置解析MyBatis原始碼
- 9個值得學習的 HTML5 效果【附原始碼】HTML原始碼
- Spring原始碼深度解析(郝佳)-學習-原始碼解析-Spring MVCSpring原始碼MVC
- 【spring原始碼學習】spring的事務管理的原始碼解析Spring原始碼
- 深入淺出Semaphore原始碼解析原始碼
- Async,Await 深入原始碼解析AI原始碼
- 深入淺出AQS原始碼解析AQS原始碼
- Java 集合Hashtable原始碼深入解析Java原始碼
- Java集合Stack原始碼深入解析Java原始碼
- mybatis原始碼學習------resultMap和sql片段的解析MyBatis原始碼SQL
- 深入解析C# List<T>的原始碼C#原始碼
- 深入理解 FilterChainProxy【原始碼篇】FilterAI原始碼
- 精選9個值得學習的 HTML5 效果【附原始碼】HTML原始碼
- Golang 學習——error 和建立 error 原始碼解析GolangError原始碼
- mybatis原始碼學習------cache-ref和cache的解析MyBatis原始碼
- 深入淺出ReentrantLock原始碼解析ReentrantLock原始碼
- 深入原始碼解析 tapable 實現原理原始碼
- 深入RxJava2 原始碼解析(一)RxJava原始碼
- Java 集合系列:Vector原始碼深入解析Java原始碼
- 深入RxJava2 原始碼解析(二)RxJava原始碼
- 深入瞭解SpringMVC原始碼解析SpringMVC原始碼
- RecyclerView 原始碼分析(一)View原始碼
- Mysql 學習篇之原始碼安裝mysqlMySql原始碼
- myBatis原始碼解析-反射篇(4)MyBatis原始碼反射
- 深入理解 HttpSecurity【原始碼篇】HTTP原始碼
- Java併發包原始碼學習系列:同步元件CountDownLatch原始碼解析Java原始碼元件CountDownLatch
- Java併發包原始碼學習系列:同步元件CyclicBarrier原始碼解析Java原始碼元件
- Java併發包原始碼學習系列:同步元件Semaphore原始碼解析Java原始碼元件
- 深入原始碼學習 Android data binding 之:回撥通知管理器 CallbackRegistry 解析原始碼Android
- Spring原始碼深度解析(郝佳)-學習-原始碼解析-基於註解注入(二)Spring原始碼
- Java併發包原始碼學習系列:JDK1.8的ConcurrentHashMap原始碼解析Java原始碼JDKHashMap