參考資料
背景介紹
RecyclerView由于其強(qiáng)大的擴(kuò)展性帖汞,現(xiàn)在已經(jīng)逐步的取代了ListView和GridView了。為了實(shí)現(xiàn)不同的布局效果凑术,我們會(huì)用到官方提供的LinearLayoutManager翩蘸、GridLayoutManager和StaggeredGridLayoutManager。但這些布局只能滿足日常需求淮逊,在一些比較復(fù)雜的布局中催首,它們就力不從心了,強(qiáng)行拼湊實(shí)現(xiàn)泄鹏,帶來的后果就是較差的體驗(yàn)和性能郎任。所以能夠自定義LayoutManager還是十分必要的,它能夠解放創(chuàng)造力备籽,構(gòu)造復(fù)雜的舶治、流暢的滑動(dòng)列表。上面幾篇參考資料中就實(shí)現(xiàn)了一些不尋常的效果胶台,我們可以看到歼疮,這些效果如果用常規(guī)的方案去實(shí)現(xiàn)將會(huì)十分蹩腳。
揭開LayoutManager中不為人知的秘密
自定義LayoutManager
主要要求我們完成三件事情:
- 計(jì)算每個(gè)ItemView的位置诈唬;
- 處理滑動(dòng)事件韩脏;
- 緩存并重用ItemView;
而我們比較重要的工作是在onLayoutChildern()
這個(gè)回調(diào)方法中完成的铸磅。
下面我們就來一一解析赡矢。
預(yù)先準(zhǔn)備
當(dāng)我們extends RecyclerView.LayoutManager是,我們會(huì)被強(qiáng)制要求重寫generateDefaultLayoutParams()
方法阅仔,如方法名字一樣吹散,我們需要提供一個(gè)默認(rèn)的LayoutParams,這里為我們的每個(gè)ItemView
提供默認(rèn)的LayoutParams
八酒,所以它能夠直接影響到我們的布局效果空民,這里我們設(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)簡單的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++) {
// 沒有會(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)用過detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見的
itemStates.put(i, false);
}
}
這段代碼邏輯簡單热鞍,它實(shí)現(xiàn)的其實(shí)就是一個(gè)簡單的垂直線性布局葫慎,當(dāng)然現(xiàn)在還不能滑動(dòng)衔彻,也沒有緩存機(jī)制。在這段代碼中偷办,我們先調(diào)用detachAndScrapAttachedViews(recycler);
將所有的ItemView標(biāo)記為Scrap狀態(tài)艰额,然后在挨個(gè)取出來,計(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++) {
// 沒有會(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)用過detachAndScrapAttachedViews(recycler),所以此時(shí)item都是不可見的
itemStates.put(i, false);
}
}
處理滑動(dòng)
先來看一下效果:
滑動(dòng)事件主要涉及到4個(gè)方法需要重寫蚓再,我們直接來看代碼:
@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:
下面來看看代碼的變化矾端,我展示了完整的代碼掏击,留心注釋。
public class CustomLayoutManager extends RecyclerView.LayoutManager {
/** 用于保存item的位置信息 */
private SparseArray<Rect> allItemRects = new SparseArray<>();
/** 用于保存item是否處于可見狀態(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++) {
// 沒有會(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)用過detachAndScrapAttachedViews(recycler)捅膘,所以此時(shí)item都是不可見的
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沒有在顯示區(qū)域篓跛,就說明需要回收
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布局出來
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的位置信息保存起來盖奈,然后在滑動(dòng)過程中通過判斷每個(gè)ItemView的位置是否和當(dāng)前RecyclerView應(yīng)該顯示的區(qū)域有重合混坞,若有就顯示它,若沒有就移除并回收钢坦。
總結(jié)
實(shí)現(xiàn)自己的自定義LayoutManager主要的三個(gè)步驟:
- 計(jì)算每個(gè)ItemView的位置究孕;
- 添加滑動(dòng)事件;
- 實(shí)現(xiàn)緩存爹凹。
我們需根據(jù)代碼多理解厨诸,多思考,然后動(dòng)手寫屬于自己的LayoutManager禾酱。
探討
最近路上留意到很多三輪摩托老司機(jī)開車十分的奔放微酬,和拉力賽有得一拼。之前坐過幾次颤陶,坐的時(shí)候因?yàn)橼s時(shí)間颗管,所以當(dāng)時(shí)感覺老司機(jī)好負(fù)責(zé)。但最近作為路人看滓走,老司機(jī)開車開的太危險(xiǎn)垦江,強(qiáng)行搶道,瘋狂按喇叭...整個(gè)是橫沖直撞的態(tài)勢搅方∫咧啵總之覺得很危險(xiǎn)。
這件事腰懂,你怎么看梗逮?
如果你覺得這篇文章對你有幫助的話,點(diǎn)贊走一走绣溜,再加個(gè)關(guān)注慷彤,互相交流下。