創(chuàng)建一個(gè)RecyclerView的LayoutManager Part 1
現(xiàn)在,如果你是一個(gè)安卓開發(fā)者,你肯定聽說過RecyclerView,一個(gè)通過支持視圖重用,使得高性能視圖的自定義實(shí)現(xiàn)更加容易的組件將會(huì)被添加到support庫(kù)中。一些人已經(jīng)對(duì)如何使用RecyclerView的基礎(chǔ)知識(shí)(動(dòng)畫)做了非常詳細(xì)的描述。所以這里不再深入講解,以下的文章可以幫助你快速了解RecyclerView:
在這一系列文章里,我們將關(guān)注如何花最少的代價(jià)去構(gòu)建一個(gè)自己的LayoutManager,從而去實(shí)現(xiàn)一些比單純的垂直或水平滾動(dòng)列表更復(fù)雜地事情。
RecyclerView Playground
這系列文章展示地所有代碼已經(jīng)上傳到GitHubRecyclerView Playground sample。這個(gè)例子包括RecyclerView的各方面內(nèi)容,從建立一個(gè)簡(jiǎn)單的列表到自定義一個(gè)LayoutManager。
本文的代碼例子是FixedGridLayoutManager:一個(gè)在水平和垂直方向都能滾動(dòng)的二維的gridLayout。
Support Library Samples
support 庫(kù)也提供了自定義LayoutManager的例子,實(shí)際上是一個(gè)自定義的垂直線性列表,sdk路徑為:
/extras/android/compatibility/samples/Support7Demos/src/com/example/android/supportv7/widget/RecyclerViewActivity.java
盡管許多的Android'L'和新的support庫(kù)可能還不在AOSP里面 ,RecyclerView支持通過導(dǎo)入JAR文件使用,你可以在這里找到它:
/extras/android/m2repository/com/android/support/recyclerview-v7/21.0.0-rc1/recyclerview-v7-21.0.0-rc1-sources.jar
The Recycler
首先來了解一下RecyclerView的API組成。當(dāng)你的RecyclerView需要回收舊的view或者從回收的view中獲取一個(gè)新的view的時(shí)候,LayoutManager發(fā)揮著至關(guān)重要的作用。
當(dāng)adapter需要的時(shí)候,RecyclerView也會(huì)直接移除view。當(dāng)你的LayoutManager需要一個(gè)新的子view時(shí),只需要調(diào)用getViewForPosition(),RecyclerView就會(huì)返回一個(gè)綁定好數(shù)據(jù)的view。RecyclerView會(huì)確定是否需要?jiǎng)?chuàng)建一個(gè)新的view,或者重用已有的廢棄的view。同時(shí)確保不可見的view及時(shí)地回收。這樣,RecyclerView就不會(huì)創(chuàng)建出多余的view。
解綁 vs 移除
當(dāng)一個(gè)view變?yōu)椴豢梢姷臅r(shí)候,有兩種方法去處理它們:解綁和移除。解綁 意味著view只需要少量的改變就可以重新顯示到視圖上。解綁的view會(huì)被用于重新綁定直接返回。這樣,這些view不需要重新創(chuàng)建或者綁定數(shù)據(jù),只要修改位置就能重新綁定到視圖上。
移除就意味著這些view不再需要了。任何被永久移除地view都應(yīng)該被放到回收池中重新使用,但也不是強(qiáng)制要求這樣做。這取決于你移除的視圖是否會(huì)被重用。
Scrap vs Recycle
RecyclerView有一個(gè)兩級(jí)的緩存系統(tǒng):scrap堆和recycle池。scrap堆是一個(gè)輕量集合,里面的view可以不經(jīng)過adapter直接回到LayoutManager。當(dāng)view暫時(shí)地解綁但可以直接重用的時(shí)候,它們通常都會(huì)被放到這里。
recycle池是由一些數(shù)據(jù)不正確的view組成的(就是顯示的數(shù)據(jù)跟所在的位置對(duì)不上),所以當(dāng)它們回到LayoutManager之前需要經(jīng)過adapter重新綁定數(shù)據(jù)。
當(dāng)LayoutManager需要一個(gè)新的view得時(shí)候,RecyclerView會(huì)先到scrap堆里面找一下有沒有位置匹配的view,如果有,就直接返回。如果沒有,RecyclerView會(huì)從recycle池中取一個(gè)合適的view并在adapter里面綁定必要的數(shù)據(jù)( RecyclerView.Adapter.bindViewHolder() 方法會(huì)被調(diào)用)然后返回。如果recycle池里沒有合適的view,就會(huì)創(chuàng)建一個(gè)新的view( RecyclerView.Adapter.createViewHolder() 方法會(huì)被調(diào)用),綁定數(shù)據(jù)然后返回。
回收的規(guī)則
如果你希望的話,LayoutManager提供的API能讓你自己去完成這些工作,所以組成的可能性有很多。一般來說,如果view只是暫時(shí)不可見并且會(huì)以相同的布局重新綁定到視圖上的就調(diào)用detachAndScrapView()。如果不再需要當(dāng)前這種布局的view就調(diào)用removeAndRecyclerView()。
創(chuàng)建的核心
LayoutManager負(fù)責(zé)實(shí)時(shí)綁定,測(cè)量和布局所有的子view。當(dāng)用戶滑動(dòng)界面的時(shí)候,LayoutManager會(huì)決定什么時(shí)候添加新的子view,什么時(shí)候解綁和舍棄舊的子view。
你需要重寫并實(shí)現(xiàn)下面的方法來創(chuàng)建一個(gè)簡(jiǎn)單可用的LayoutManager。
generateDefaultLayoutParams()
實(shí)際上這是唯一一個(gè)必須重寫的方法。這個(gè)方法的實(shí)現(xiàn)非常簡(jiǎn)單,只需要返回一個(gè)你想要應(yīng)用到RecyclerView的所有子view的LayoutParams實(shí)例即可。在getViewForPosition()這個(gè)方法執(zhí)行完成前,這個(gè)返回值會(huì)應(yīng)用到每一個(gè)子view。
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(
RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
onLayoutChildren()
這是LayoutManager的一個(gè)重要的方法。當(dāng)你的view需要初始化布局,或者適配器數(shù)據(jù)發(fā)生改變(或者更換適配器)的時(shí)候,這個(gè)方法就會(huì)被調(diào)用。并不是每一個(gè)布局的改變都會(huì)調(diào)用這個(gè)方法。在這個(gè)方法里你可以重新布局子view的視圖和改變數(shù)據(jù)。
接下來,我們會(huì)了解到當(dāng)adapter刷新的時(shí)候,它是如何去刷新當(dāng)前可見的item的布局的?,F(xiàn)在,我們會(huì)簡(jiǎn)單地梳理一下子view的布局流程。以下是一個(gè)簡(jiǎn)單地FixedGridLayoutManager例子:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
RecyclerView.State state) {
//測(cè)量view
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
/*
* 我們假設(shè)所有的子view都是一樣大的,這樣就可以推算出接下來的大小了
*/
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
detachAndScrapView(scrap, recycler);
updateWindowSizing();
int childLeft;
int childTop;
/*
* 重置第一個(gè)可見的item的下標(biāo)
*/
mFirstVisiblePosition = 0;
childLeft = childTop = 0;
//清除所有已經(jīng)綁定的view
detachAndScrapAttachedViews(recycler);
//填充recyclerView
fillGrid(DIRECTION_NONE, childLeft, childTop, recycler);
}
這里我們做了一些設(shè)置(為了簡(jiǎn)單起見,layoutManager假設(shè)所有的子view都是一樣大的)同時(shí)確保所有的view都在scrap堆里。為了可重用,我將大部分的工作都寫在了fillGrid()方法里了。當(dāng)RecyclerView滾動(dòng)時(shí),我們會(huì)看到這個(gè)方法會(huì)被調(diào)用去刷新可見的view。
就像ViewGroup的實(shí)現(xiàn)一樣,你需要測(cè)量和布局所有從RecyclerView獲得的view。這些工作都需要你自己去完成。
一般來說,你需要在這個(gè)方法完成的事有以下幾樣:
- 在滑動(dòng)事件觸發(fā)后,檢測(cè)當(dāng)前綁定綁定視圖的偏移位置
- 確定滑動(dòng)的距離是否有足夠去添加從RecyclerView中獲取的view
- 確定是否存在不再需要顯示的view,有的話就移除
- 確定是否有應(yīng)該被調(diào)整的view。為了跟適配器的位置信息對(duì)應(yīng),可能需要修改這些view的索引信息
填充RecyclerView的主要步驟我們已經(jīng)寫在FxiedGridLayoutManager.fillGrid(),layoutManager是將view按行從左到右排列:
- 緩存所有現(xiàn)有的view,解綁它們便于后面重新綁定。
SparseArray viewCache = new SparseArray(getChildCount());
//...
if (getChildCount() != 0) {
//...
//緩存所有的view
for (int i=0; i < getChildCount(); i++) {
int position = positionOfIndex(i);
final View child = getChildAt(i);
viewCache.put(position, child);
}
//暫時(shí)解綁所有的view
// Views we still need will be added back at the proper index.
for (int i=0; i < viewCache.size(); i++) {
detachView(viewCache.valueAt(i));
}
}
- 測(cè)量和布局當(dāng)前可見的view。部分view是需要從緩存獲取重新綁定的,還有部分是需要從RecyclerView中獲取的。
for (int i = 0; i < getVisibleChildCount(); i++) {
//...
//Layout this position
View view = viewCache.get(nextPosition);
if (view == null) {
/*
* Recyclerview會(huì)創(chuàng)建一個(gè)新的view或者重用一個(gè)view,并且adapter會(huì)
* 為我們綁定好數(shù)據(jù)
*/
view = recycler.getViewForPosition(nextPosition);
addView(view);
/*
*測(cè)量和布局新的view
*/
measureChildWithMargins(view, 0, 0);
layoutDecorated(view, leftOffset, topOffset,
leftOffset + mDecoratedChildWidth,
topOffset + mDecoratedChildHeight);
} else {
//重新綁定數(shù)據(jù)
attachView(view);
viewCache.remove(nextPosition);
}
//...
}
- 最后,回收所有在第一步解綁的不再可見的view便于后面再使用
for (int i=0; i < viewCache.size(); i++) {
recycler.recycleView(viewCache.valueAt(i));
}
我們解綁所有的view然后重新綁定我們需要的是為了確保所有子view的索引(getChildAt())都是正確的。我們希望可見的view是從左上角第0個(gè)開始到右下角getChildCount() -1個(gè)結(jié)束。當(dāng)我們?cè)趦蓚€(gè)方向上滑動(dòng),子view重新綁定時(shí),順序就會(huì)變得不可靠。我們需要這個(gè)順序去確保每一個(gè)子view的位置。在簡(jiǎn)單的LayoutManager(LinearLayoutManager),子view可以簡(jiǎn)單地插到列表尾部的,這種操作就不是必須的了。
添加交互動(dòng)作
到這一步,我們已經(jīng)有一個(gè)初始化好的布局,但是卻無法移動(dòng)。RecyclerView主要的功能點(diǎn)就是在用戶滑動(dòng)數(shù)據(jù)集的時(shí)候動(dòng)態(tài)地提供view。我們需要重寫幾個(gè)方法讓它能夠滑動(dòng)。
canScrollHorizontally() & canScrollVertically()
這兩個(gè)方法很簡(jiǎn)單,如果你想支持該方向的滑動(dòng),就返回true,否則返回false。
@Override
public boolean canScrollVertically() {
//We do allow scrolling
return true;
}
scrollHorizontallyBy() & scrollVerticallyBy()
這里你需要實(shí)現(xiàn)內(nèi)容滾動(dòng)的邏輯?;瑒?dòng)和慣性滑動(dòng)的邏輯RecyclerView已經(jīng)幫我們處理了,所以不需要處理手勢(shì)監(jiān)聽和滑動(dòng)事件。在這兩個(gè)方法里你需要做下面三樣事情:
1.為所有的子view移動(dòng)適當(dāng)?shù)木嚯x。
2.確定滑動(dòng)時(shí)是否需要添加或者移除view去填充視圖
3.返回實(shí)際的滑動(dòng)距離,系統(tǒng)用這個(gè)值去判斷什么時(shí)候碰到了邊界。
在FixedGridLayoutManager里,這兩個(gè)方法很相似,下面是垂直滑動(dòng)的實(shí)現(xiàn):
@Override
public int scrollVerticallyBy(int dy,
RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
//獲取第一個(gè)view
final View topView = getChildAt(0);
//獲取最后一個(gè)view
final View bottomView = getChildAt(getChildCount()-1);
//數(shù)據(jù)集太小不能滑動(dòng),直接返回
int viewSpan = getDecoratedBottom(bottomView) - getDecoratedTop(topView);
if (viewSpan <= getVerticalSpace()) {
//We cannot scroll in either direction
return 0;
}
int delta;
int maxRowCount = getTotalRowCount();
boolean topBoundReached = getFirstVisibleRow() == 0;
boolean bottomBoundReached = getLastVisibleRow() >= maxRowCount;
if (dy > 0) { // 上滑
//邊界檢測(cè)
if (bottomBoundReached) {
//已經(jīng)在底部,限制滑動(dòng)距離
int bottomOffset;
if (rowOfIndex(getChildCount() - 1) >= (maxRowCount - 1)) {
//已經(jīng)在底部,計(jì)算距離
bottomOffset = getVerticalSpace()
- getDecoratedBottom(bottomView) + getPaddingBottom();
} else {
/*
* 一個(gè)view不是完全可見,計(jì)算實(shí)際的滑動(dòng)距離
*/
bottomOffset = getVerticalSpace()
- (getDecoratedBottom(bottomView) + mDecoratedChildHeight)
+ getPaddingBottom();
}
delta = Math.max(-dy, bottomOffset);
} else {
//不在底部,沒有限制
delta = -dy;
}
} else { // 下滑
//是否到頂部
if (topBoundReached) {
int topOffset = -getDecoratedTop(topView) + getPaddingTop();
delta = Math.min(-dy, topOffset);
} else {
delta = -dy;
}
}
offsetChildrenVertical(delta);
if (dy > 0) {
if (getDecoratedBottom(topView) < 0 && !bottomBoundReached) {
fillGrid(DIRECTION_DOWN, recycler);
} else if (!bottomBoundReached) {
fillGrid(DIRECTION_NONE, recycler);
}
} else {
if (getDecoratedTop(topView) > 0 && !topBoundReached) {
fillGrid(DIRECTION_UP, recycler);
} else if (!topBoundReached) {
fillGrid(DIRECTION_NONE, recycler);
}
}
/*
* 返回值決定是否到邊界
* 如果返回值跟傳過來的不一樣
* RecyclerView就會(huì)顯示一個(gè)到達(dá)邊界的效果
*/
return -delta;
}
注意,這里的是距離(dx/dy)的增量,這個(gè)參數(shù)決定了滑動(dòng)的距離(方向)是否會(huì)超過內(nèi)容的長(zhǎng)度,觸碰到邊界。如果會(huì),我們需要計(jì)算出視圖滑動(dòng)的實(shí)際距離。
我們必須在這個(gè)方法里手動(dòng)移動(dòng)子views,offsetChildrenVertical() 和 offsetChildrenHorizontal()幫助我們協(xié)調(diào)view的移動(dòng)。如果你不這樣做,你的view就不能移動(dòng)。當(dāng)移動(dòng)完這些view后,根據(jù)滑動(dòng)的方向會(huì)觸發(fā)另一個(gè)操作去用view重新填充視圖。
最后,我們返回實(shí)際使用的滑動(dòng)距離。RecyclerView使用這個(gè)值去決定滾動(dòng)時(shí)是否需要繪制到達(dá)底部或頂部的邊界效果。一般來說,如果返回的值跟傳遞過來的dx/dy不相等,就需要繪制邊界效果。如果你返回一個(gè)不正確的值,系統(tǒng)就會(huì)把它變成一個(gè)很大的值,就會(huì)錯(cuò)誤地顯示出邊界效果。
除了繪制邊界效果外,這個(gè)返回值還用于決定是否取消慣性滑動(dòng)。返回錯(cuò)誤的值,系統(tǒng)會(huì)認(rèn)為你已經(jīng)觸碰到邊界,慣性滑動(dòng)就不會(huì)執(zhí)行。
Just Getting Warmed Up
現(xiàn)在,我們已經(jīng)實(shí)現(xiàn)了基礎(chǔ)功能,雖然缺少了一些細(xì)節(jié),但視圖的滑動(dòng)和view的回收都已經(jīng)實(shí)現(xiàn)了。接下來將會(huì)討論更多關(guān)于自定義LayoutManager的內(nèi)容。下一篇文章我們會(huì)處理decorations,數(shù)據(jù)的改變和滑動(dòng)到指定位置的實(shí)現(xiàn)。
ps:這是一篇比較舊的文章了,第一次翻譯,有什么理解不對(duì)的,不通順的地方,歡迎在下方留言指出,我會(huì)更改的。