參考資料
背景介紹
RecyclerView由于其強(qiáng)大的擴(kuò)展性,現(xiàn)在已經(jīng)逐步的取代了ListView和GridView了。為了實(shí)現(xiàn)不同的布局效果,我們會(huì)用到官方提供的LinearLayoutManager、GridLayoutManager和StaggeredGridLayoutManager。但這些布局只能滿足日常需求,在一些比較復(fù)雜的布局中,它們就力不從心了,強(qiáng)行拼湊實(shí)現(xiàn),帶來(lái)的后果就是較差的體驗(yàn)和性能。所以能夠自定義LayoutManager還是十分必要的,它能夠解放創(chuàng)造力,構(gòu)造復(fù)雜的、流暢的滑動(dòng)列表。上面幾篇參考資料中就實(shí)現(xiàn)了一些不尋常的效果,我們可以看到,這些效果如果用常規(guī)的方案去實(shí)現(xiàn)將會(huì)十分蹩腳。
揭開(kāi)LayoutManager中不為人知的秘密
自定義LayoutManager
主要要求我們完成三件事情:
- 計(jì)算每個(gè)ItemView的位置;
- 處理滑動(dòng)事件;
- 緩存并重用ItemView;
而我們比較重要的工作是在onLayoutChildern()
這個(gè)回調(diào)方法中完成的。
下面我們就來(lái)一一解析。
預(yù)先準(zhǔn)備
當(dāng)我們extends RecyclerView.LayoutManager是,我們會(huì)被強(qiáng)制要求重寫(xiě)generateDefaultLayoutParams()
方法,如方法名字一樣,我們需要提供一個(gè)默認(rèn)的LayoutParams,這里為我們的每個(gè)ItemView
提供默認(rèn)的LayoutParams
,所以它能夠直接影響到我們的布局效果,這里我們?cè)O(shè)置成WRAP_CONTENT
,讓ItemView
獲得決定權(quán)。
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT,
RecyclerView.LayoutParams.WRAP_CONTENT);
}
計(jì)算ItemView的位置
1.實(shí)現(xiàn)簡(jiǎn)單的LayoutManager
先看效果圖:
再看代碼:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
super.onLayoutChildren(recycler, state);
// 先把所有的View先從RecyclerView中detach掉,然后標(biāo)記為"Scrap"狀態(tài),表示這些View處于可被重用狀態(tài)(非顯示中)。
// 實(shí)際就是把View放到了Recycler中的一個(gè)集合中。
detachAndScrapAttachedViews(recycler);
calculateChildrenSite(recycler);
// 回收和填充Item
recycleAndFillView(recycler, state);
}
private void calculateChildrenSite(RecyclerView.Recycler recycler) {
totalHeight = 0;
boolean needNew = true;
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
// 沒(méi)有會(huì)創(chuàng)建
if (needNew) {
View view = recycler.getViewForPosition(i);
measureChildWithMargins(view, 0, 0);
calculateItemDecorationsForChild(view, new Rect());
width = getDecoratedMeasuredWidth(view);
height = getDecoratedMeasuredHeight(view);
addView(view);
}
if (totalHeight > getHeight() + height) {
needNew = false;
}
Rect mTmpRect = allItemRects.get(i);
if (mTmpRect == null) {
mTmpRect = new Rect();
}
mTmpRect.set(0, totalHeight, DisplayUtils.getScreenWidth(), totalHeight + height);
totalHeight = totalHeight + height;
// 保存ItemView的位置信息
allItemRects.put(i, mTmpRect);
// 由于之前調(diào)用過(guò)detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見(jiàn)的
itemStates.put(i, false);
}
}
這段代碼邏輯簡(jiǎn)單,它實(shí)現(xiàn)的其實(shí)就是一個(gè)簡(jiǎn)單的垂直線性布局,當(dāng)然現(xiàn)在還不能滑動(dòng),也沒(méi)有緩存機(jī)制。在這段代碼中,我們先調(diào)用detachAndScrapAttachedViews(recycler);
將所有的ItemView標(biāo)記為Scrap狀態(tài),然后在挨個(gè)取出來(lái),計(jì)算他們應(yīng)該布局到什么位置,并用成員變量totalHeight記錄總高度,最后調(diào)用recycleAndFillView()
將ItemView布局上去。
2.兩列式的LayoutManager
先看效果圖:
有了上例的基礎(chǔ),我們只需要稍作調(diào)整,直接看下面代碼,注意注釋部分。
private void calculateChildrenSite(RecyclerView.Recycler recycler) {
totalHeight = 0;
boolean needNew = true;
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
// 沒(méi)有會(huì)創(chuàng)建
if (needNew) {
View view = recycler.getViewForPosition(i);
measureChildWithMargins(view, DisplayUtils.getScreenWidth() / 2, 0);
calculateItemDecorationsForChild(view, new Rect());
width = getDecoratedMeasuredWidth(view);
height = getDecoratedMeasuredHeight(view);
addView(view);
}
if (totalHeight > getHeight() + height) {
needNew = false;
}
Rect mTmpRect = allItemRects.get(i);
if (mTmpRect == null) {
mTmpRect = new Rect();
}
if (i % 2 == 0) { // 當(dāng)i能被2整除時(shí),是左,否則是右。
// 左
mTmpRect.set(0, totalHeight, DisplayUtils.getScreenWidth() / 2, totalHeight + height);
} else {
// 右,需要換行
mTmpRect.set(DisplayUtils.getScreenWidth() / 2, totalHeight, DisplayUtils.getScreenWidth(),
totalHeight + height);
totalHeight = totalHeight + height;
}
// 保存ItemView的位置信息
allItemRects.put(i, mTmpRect);
// 由于之前調(diào)用過(guò)detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見(jiàn)的
itemStates.put(i, false);
}
}
處理滑動(dòng)
先來(lái)看一下效果:
滑動(dòng)事件主要涉及到4個(gè)方法需要重寫(xiě),我們直接來(lái)看代碼:
@Override
public boolean canScrollVertically() {
//返回true表示可以縱向滑動(dòng)
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//列表向下滾動(dòng)dy為正,列表向上滾動(dòng)dy為負(fù),這點(diǎn)與Android坐標(biāo)系保持一致。
//實(shí)際要滑動(dòng)的距離
int travel = dy;
LogUtils.e("dy = " + dy);
//如果滑動(dòng)到最頂部
if (verticalScrollOffset + dy < 0) {
travel = -verticalScrollOffset;
} else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {//如果滑動(dòng)到最底部
travel = totalHeight - getVerticalSpace() - verticalScrollOffset;
}
//將豎直方向的偏移量+travel
verticalScrollOffset += travel;
// 調(diào)用該方法通知view在y方向上移動(dòng)指定距離
offsetChildrenVertical(-travel);
return travel;
}
private int getVerticalSpace() {
//計(jì)算RecyclerView的可用高度,除去上下Padding值
return getHeight() - getPaddingBottom() - getPaddingTop();
}
@Override
public boolean canScrollHorizontally() {
//返回true表示可以橫向滑動(dòng)
return super.canScrollHorizontally();
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
//在這個(gè)方法中處理水平滑動(dòng)
return super.scrollHorizontallyBy(dx, recycler, state);
}
緩存并重用ItemView
在上面代碼的基礎(chǔ)上我們稍作改動(dòng),加入緩存,先看下面的log信息,它顯示雖然有100個(gè)Item,但childCount穩(wěn)定在26:
下面來(lái)看看代碼的變化,我展示了完整的代碼,留心注釋。
public class CustomLayoutManager extends RecyclerView.LayoutManager {
/** 用于保存item的位置信息 */
private SparseArray<Rect> allItemRects = new SparseArray<>();
/** 用于保存item是否處于可見(jiàn)狀態(tài)的信息 */
private SparseBooleanArray itemStates = new SparseBooleanArray();
public int totalHeight = 0;
private int verticalScrollOffset;
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
super.onLayoutChildren(recycler, state);
detachAndScrapAttachedViews(recycler);
/* 這個(gè)方法主要用于計(jì)算并保存每個(gè)ItemView的位置 */
calculateChildrenSite(recycler);
recycleAndFillView(recycler, state);
}
private void calculateChildrenSite(RecyclerView.Recycler recycler) {
totalHeight = 0;
boolean needNew = true;
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
// 沒(méi)有會(huì)創(chuàng)建
if (needNew) {
View view = recycler.getViewForPosition(i);
measureChildWithMargins(view, DisplayUtils.getScreenWidth() / 2, 0);
calculateItemDecorationsForChild(view, new Rect());
width = getDecoratedMeasuredWidth(view);
height = getDecoratedMeasuredHeight(view);
addView(view);
}
if (totalHeight > getHeight() + height) {
needNew = false;
}
Rect mTmpRect = allItemRects.get(i);
if (mTmpRect == null) {
mTmpRect = new Rect();
}
if (i % 2 == 0) { // 當(dāng)i能被2整除時(shí),是左,否則是右。
// 左
mTmpRect.set(0, totalHeight, DisplayUtils.getScreenWidth() / 2, totalHeight + height);
} else {
// 右,需要換行
mTmpRect.set(DisplayUtils.getScreenWidth() / 2, totalHeight, DisplayUtils.getScreenWidth(),
totalHeight + height);
totalHeight = totalHeight + height;
}
// 保存ItemView的位置信息
allItemRects.put(i, mTmpRect);
// 由于之前調(diào)用過(guò)detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見(jiàn)的
itemStates.put(i, false);
}
}
private void recycleAndFillView(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() <= 0 || state.isPreLayout()) {
return;
}
// 當(dāng)前scroll offset狀態(tài)下的顯示區(qū)域
Rect displayRect= new Rect(0, verticalScrollOffset, getHorizontalSpace(),
verticalScrollOffset + getVerticalSpace());
/**
* 將滑出屏幕的Items回收到Recycle緩存中
*/
Rect childRect = new Rect();
for (int i = 0; i < getItemCount(); i++) {
//這個(gè)方法獲取的是RecyclerView中的View,注意區(qū)別Recycler中的View
//這獲取的是實(shí)際的View
View child = recycler.getViewForPosition(i);
//下面幾個(gè)方法能夠獲取每個(gè)View占用的空間的位置信息,包括ItemDecorator
childRect.left = getDecoratedLeft(child);
childRect.top = getDecoratedTop(child);
childRect.right = getDecoratedRight(child);
childRect.bottom = getDecoratedBottom(child);
//如果Item沒(méi)有在顯示區(qū)域,就說(shuō)明需要回收
if (!Rect.intersects(displayRect, childRect)) {
//移除并回收掉滑出屏幕的View
removeAndRecycleView(child, recycler);
itemStates.put(i, false); //更新該View的狀態(tài)為未依附
}
}
//重新顯示需要出現(xiàn)在屏幕的子View
for (int i = 0; i < getItemCount(); i++) {
//判斷ItemView的位置和當(dāng)前顯示區(qū)域是否重合
if (Rect.intersects(displayRect, allItemRects.get(i))) {
//獲得Recycler中緩存的View
View itemView = recycler.getViewForPosition(i);
measureChildWithMargins(itemView, DisplayUtils.getScreenWidth() / 2, 0);
//添加View到RecyclerView上
addView(itemView);
//取出先前存好的ItemView的位置矩形
Rect rect = allItemRects.get(i);
//將這個(gè)item布局出來(lái)
layoutDecoratedWithMargins(itemView,
rect.left,
rect.top - verticalScrollOffset, //因?yàn)楝F(xiàn)在是復(fù)用View,所以想要顯示在
rect.right,
rect.bottom - verticalScrollOffset);
itemStates.put(i, true); //更新該View的狀態(tài)為依附
}
}
LogUtils.e("itemCount = " + getChildCount());
}
@Override
public boolean canScrollVertically() {
// 返回true表示可以縱向滑動(dòng)
return true;
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//每次滑動(dòng)時(shí)先釋放掉所有的View,因?yàn)楹竺嬲{(diào)用recycleAndFillView()時(shí)會(huì)重新addView()。
detachAndScrapAttachedViews(recycler);
// 列表向下滾動(dòng)dy為正,列表向上滾動(dòng)dy為負(fù),這點(diǎn)與Android坐標(biāo)系保持一致。
// 實(shí)際要滑動(dòng)的距離
int travel = dy;
LogUtils.e("dy = " + dy);
// 如果滑動(dòng)到最頂部
if (verticalScrollOffset + dy < 0) {
travel = -verticalScrollOffset;
} else if (verticalScrollOffset + dy > totalHeight - getVerticalSpace()) {// 如果滑動(dòng)到最底部
travel = totalHeight - getVerticalSpace() - verticalScrollOffset;
}
// 調(diào)用該方法通知view在y方向上移動(dòng)指定距離
offsetChildrenVertical(-travel);
recycleAndFillView(recycler, state); //回收并顯示View
// 將豎直方向的偏移量+travel
verticalScrollOffset += travel;
return travel;
}
private int getVerticalSpace() {
// 計(jì)算RecyclerView的可用高度,除去上下Padding值
return getHeight() - getPaddingBottom() - getPaddingTop();
}
@Override
public boolean canScrollHorizontally() {
// 返回true表示可以橫向滑動(dòng)
return super.canScrollHorizontally();
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
// 在這個(gè)方法中處理水平滑動(dòng)
return super.scrollHorizontallyBy(dx, recycler, state);
}
public int getHorizontalSpace() {
return getWidth() - getPaddingLeft() - getPaddingRight();
}
}
實(shí)現(xiàn)緩存最主要的就是先把每個(gè)ItemView的位置信息保存起來(lái),然后在滑動(dòng)過(guò)程中通過(guò)判斷每個(gè)ItemView的位置是否和當(dāng)前RecyclerView應(yīng)該顯示的區(qū)域有重合,若有就顯示它,若沒(méi)有就移除并回收。
總結(jié)
實(shí)現(xiàn)自己的自定義LayoutManager主要的三個(gè)步驟:
- 計(jì)算每個(gè)ItemView的位置;
- 添加滑動(dòng)事件;
- 實(shí)現(xiàn)緩存。
我們需根據(jù)代碼多理解,多思考,然后動(dòng)手寫(xiě)屬于自己的LayoutManager。
探討
最近路上留意到很多三輪摩托老司機(jī)開(kāi)車(chē)十分的奔放,和拉力賽有得一拼。之前坐過(guò)幾次,坐的時(shí)候因?yàn)橼s時(shí)間,所以當(dāng)時(shí)感覺(jué)老司機(jī)好負(fù)責(zé)。但最近作為路人看,老司機(jī)開(kāi)車(chē)開(kāi)的太危險(xiǎn),強(qiáng)行搶道,瘋狂按喇叭...整個(gè)是橫沖直撞的態(tài)勢(shì)??傊X(jué)得很危險(xiǎn)。
這件事,你怎么看?
如果你覺(jué)得這篇文章對(duì)你有幫助的話,點(diǎn)贊走一走,再加個(gè)關(guān)注,互相交流下。