轉(zhuǎn)自RecyclerView系列之五回收復(fù)用實(shí)現(xiàn)方式二
在自定義LayoutManager之復(fù)用與回收一,我們已經(jīng)實(shí)現(xiàn)了自定義LayoutManager的復(fù)用與回收,但是我們直接調(diào)用了offsetChildrenVertical(-travel)
來(lái)實(shí)現(xiàn)了item的滾動(dòng),這個(gè)方法僅適用于每個(gè)item在移動(dòng)時(shí)沒(méi)什么特殊的情況,當(dāng)在滑動(dòng)時(shí)需要修改每個(gè)item的角度、透明度等情況時(shí),單純使用offsetChildrenVertical(-travel)
是不可行的。針對(duì)這種情況,本文介紹實(shí)現(xiàn)復(fù)用回收的第二種方式。
在本節(jié)中,我們最終實(shí)現(xiàn)的效果如下圖所示:
從效果中可以看出,在滑動(dòng)過(guò)程中同時(shí)每個(gè)item繞y軸旋轉(zhuǎn),因?yàn)榇蟛糠衷砗蜕衔闹械腃ustomLayoutManager相同,只需在上文的基礎(chǔ)上進(jìn)行修改即可。
1、初步實(shí)現(xiàn)
1.1、實(shí)現(xiàn)原理
在這里,我們需要去掉offsetChildrenVertical(-travel)
滑動(dòng)item,然后自己去布局每個(gè)item。很明顯,我們只需要處理滑動(dòng),所以onLayoutChildren初始化布局邏輯不需修改,只需要修改scrollVerticallyBy()
方法中邏輯。
在滑動(dòng)過(guò)程中,有兩種item需要重新布局:
- 第一種:原來(lái)已經(jīng)在屏幕中的item
- 第二種:新增的item
所以這里就涉及到如何處理已經(jīng)在屏幕上的item和新增item的重繪問(wèn)題,這里可以效仿onLayoutChildren方法的實(shí)現(xiàn)方式,先調(diào)用detachAndScrapAttachedViews(recycler)
方法分離屏幕上的item,然后再重繪所有item。
那么應(yīng)該重繪哪些item呢?這里依然分兩種情況:
- 1、當(dāng)向上滾動(dòng)時(shí),頂部item向上移動(dòng),底部空出空白,所以我們只需從當(dāng)前顯示的第一個(gè)item向下遍歷直到結(jié)束。
- 2、當(dāng)向下滾動(dòng)時(shí),底部item向下移動(dòng),頂部留出空白,此時(shí)只需要從當(dāng)前顯示的最后一個(gè)item向上遍歷,直接index=0為止。
1.2、改造CustomLayoutManager
上面已經(jīng)說(shuō)了,只需要修改scrollVerticallyBy()
中邏輯即可。其中頂部、底部的邊界判斷,以及回收的邏輯不需要修改。
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() <= 0) {
return dy;
}
int travel = dy;
//如果滑動(dòng)到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑動(dòng)到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
if (getDecoratedBottom(child) - travel < 0) {
removeAndRecycleView(child, recycler);
continue;
}
} else if (travel < 0) {//回收當(dāng)前屏幕,下越界的View
if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
removeAndRecycleView(child, recycler);
continue;
}
}
}
…………
}
在回收之后,調(diào)用detachAndScrapAttachedViews(recycler);
將屏幕上可見(jiàn)的item進(jìn)行剝離,在剝離之前需要先記錄當(dāng)前屏幕顯示的第一個(gè)item和最后一個(gè)item的索引,否則在調(diào)用detachAndScrapAttachedViews(recycler);
之后,調(diào)用getChildAt(i)
就會(huì)返回null。
View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
detachAndScrapAttachedViews(recycler);
mSumDy += travel;
Rect visibleRect = getVisibleArea();
這里需要注意的是,我們?cè)谒胁季植僮髦埃葘⒁苿?dòng)距離進(jìn)行累加。因?yàn)楹竺嫖覀冊(cè)诓季謎tem時(shí),會(huì)棄用offsetChildrenVertical(-travel)
移動(dòng)item,而在布局時(shí)直接將item布局在新位置。最后,因?yàn)槲覀円呀?jīng)累加了mSumDy,所以我們需要改造getVisibleArea(),將原來(lái)getVisibleArea(int dy)中累加dy的操作去掉:
private Rect getVisibleArea() {
Rect result = new Rect(getPaddingLeft(), getPaddingTop() + mSumDy, getWidth() + getPaddingRight(), getVerticalSpace() + mSumDy);
return result;
}
接下來(lái),就是布局屏幕上的所有item,同樣是分情況:
if (travel >= 0) {
int minPos = getPosition(firstView);
for (int i = minPos; i < getItemCount(); i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
}
//else
// break;
}
}
注意:不能在不滿足Rect.intersects(visibleRect, rect)
條件時(shí)直接break。比如在向上滑動(dòng)travel前,當(dāng)前屏幕上有三個(gè)可見(jiàn)的item且此時(shí)第一個(gè)item馬上要滑出屏幕,在向上滑動(dòng)travel時(shí),第一個(gè)item不在屏幕內(nèi)了,此時(shí)會(huì)執(zhí)行注釋處的else代碼執(zhí)行break,后面可見(jiàn)的item將不能布局在屏幕上,由于在布局前調(diào)用了detachAndScrapAttachedViews(recycler)
剝離了item,所以此時(shí)整個(gè)屏幕一片空白。
所以當(dāng)travel>0表示向上滑動(dòng),就需要從當(dāng)前顯示的第一個(gè)item開始遍歷,由于我們不知道到哪里結(jié)束,所以就是用最后一個(gè)item的索引(getItemCount
)作為結(jié)束位置。
當(dāng)然大家在這里也可以優(yōu)化,可以使用下面的語(yǔ)句:
int max = minPos + 50 < getItemCount() ? minPos + 50 : getItemCount();
即從第一個(gè)item向后累加50項(xiàng),如果最后的索引小于getItemCount()
,就用minPos + 50
作為結(jié)束值,否則用getItemCount()
作為結(jié)束值。這里的50并不是固定的,可以根據(jù)實(shí)際情況進(jìn)行修改。
然后在dy<0時(shí),表示向下滾動(dòng)
if (travel >= 0) {
…………
} else {
int maxPos = getPosition(lastView);
for (int i = maxPos; i >= 0; i--) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child, 0);
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
}
}
}
因?yàn)槭窍蛳聺L動(dòng),所以頂部新增,底部回收,所以我們需要從當(dāng)前底部可見(jiàn)的最后一個(gè)item向上遍歷,將每個(gè)item布局到新位置,但什么時(shí)候截止呢?我們同樣可以向上減50:
int min = maxPos - 50 >= 0 ? maxPos - 50 : 0;
這里我為了方便理解,還是一直遍歷到索引0;
代碼到這里就改造完了,scrollVerticallyBy的核心代碼如下(除去到頂、頂?shù)着袛嗪驮浇缁厥眨?/p>
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//到頂/到底判斷
…………
//回收越界子View
…………
View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
detachAndScrapAttachedViews(recycler);
if (travel >= 0) {
int minPos = getPosition(firstView);
for (int i = minPos; i < getItemCount(); i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
}
}
} else {
int maxPos = getPosition(lastView);
for (int i = maxPos; i >= 0; i--) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child, 0);
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
}
}
}
return travel;
}
下面就可以在布局item時(shí),調(diào)用child.setRotationY(child.getRotationY()+1);
將它的圍繞Y軸的旋轉(zhuǎn)度數(shù)加1,所以每滾動(dòng)一次,就會(huì)旋轉(zhuǎn)度數(shù)加1.這樣就實(shí)現(xiàn)了開篇的效果了。
2、繼續(xù)優(yōu)化:回收時(shí)布局
在上部分中,我們通過(guò)先使用detachAndScrapAttachedViews(recycler)
將所有item離屏緩存,然后通過(guò)再重新布局所有item的方法來(lái)實(shí)現(xiàn)回收復(fù)用。
但是這里有個(gè)問(wèn)題,我們能不能把已經(jīng)在屏幕上的item直接布局呢?這樣就省去了先離屏緩存再重新布局的操作,提高了性能。
那這個(gè)直接布局已經(jīng)在屏幕上的item的步驟,放在哪里呢?我們知道,我們?cè)诨厥赵浇鏸tem時(shí),會(huì)遍歷所有的可見(jiàn)item,所以我們可以把它放在回收越界時(shí),如果越界就回收,如果沒(méi)越界就重新布局:
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
int position = getPosition(child);
Rect rect = mItemRects.get(position);
if (!Rect.intersects(rect, visibleRect)) {
removeAndRecycleView(child, recycler);
}else {
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
child.setRotationY(child.getRotationY() + 1);
}
}
因?yàn)楹竺嫖覀冞€需要布局所有Item,很明顯,在全部布局時(shí),這些已經(jīng)布局過(guò)的item就需要排除掉,所以我們需要一個(gè)變量來(lái)保存在這里哪些item已經(jīng)布局好了:
所以,我們先申請(qǐng)一個(gè)成員變量:
private SparseBooleanArray mHasAttachedItems = new SparseBooleanArray();
然后在onLayoutChildren中初始化:
public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
…………
mHasAttachedItems.clear();
mItemRects.clear();
…………
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(0, offsetY, mItemWidth, offsetY + mItemHeight);
mItemRects.put(i, rect);
mHasAttachedItems.put(i, false);
offsetY += mItemHeight;
}
…………
}
在onLayoutChildren中,先將它清空,然后在遍歷所有item時(shí),把所有item所對(duì)應(yīng)的值設(shè)置為false,表示所有item都沒(méi)有被重新布局。
然后在回收越界holdview時(shí),將已經(jīng)重新布局的item置為true.將被回收的item,回收時(shí)設(shè)置為false;
public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
…………
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
int position = getPosition(child);
Rect rect = mItemRects.get(position);
if (!Rect.intersects(rect, visibleRect)) {
removeAndRecycleView(child, recycler);
mHasAttachedItems.put(position,false);
} else {
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
child.setRotationY(child.getRotationY() + 1);
mHasAttachedItems.put(i, true);
}
}
…………
}
最后在布局所有item時(shí),添加判斷當(dāng)前的item是否已經(jīng)被布局,沒(méi)布局的item再布局,需要注意的是,在布局后,需要將mHasAttachedItems中對(duì)應(yīng)位置改為true,表示已經(jīng)在布局中了。
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
//到頂/到底判斷
…………
//回收越界子View
…………
View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
if (travel >= 0) {
int minPos = getPosition(firstView);
for (int i = minPos; i < getItemCount(); i++) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child);
measureChildWithMargins(child, 0, 0);
layoutDecorated(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
mHasAttachedItems.put(i,true);
}
}
} else {
int maxPos = getPosition(lastView);
for (int i = maxPos; i >= 0; i--) {
Rect rect = mItemRects.get(i);
if (Rect.intersects(visibleRect, rect)) {
View child = recycler.getViewForPosition(i);
addView(child, 0);
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
mHasAttachedItems.put(i,true);
}
}
}
return travel;
}
完整onLayoutChildren和scrollVerticallyBy的代碼如下
public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0) {//沒(méi)有Item,界面空著吧
detachAndScrapAttachedViews(recycler);
return;
}
mHasAttachedItems.clear();
mItemRects.clear();
detachAndScrapAttachedViews(recycler);
//將item的位置存儲(chǔ)起來(lái)
View childView = recycler.getViewForPosition(0);
measureChildWithMargins(childView, 0, 0);
mItemWidth = getDecoratedMeasuredWidth(childView);
mItemHeight = getDecoratedMeasuredHeight(childView);
int visibleCount = getVerticalSpace() / mItemHeight;
//定義豎直方向的偏移量
int offsetY = 0;
for (int i = 0; i < getItemCount(); i++) {
Rect rect = new Rect(0, offsetY, mItemWidth, offsetY + mItemHeight);
mItemRects.put(i, rect);
mHasAttachedItems.put(i, false);
offsetY += mItemHeight;
}
for (int i = 0; i < visibleCount; i++) {
Rect rect = mItemRects.get(i);
View view = recycler.getViewForPosition(i);
addView(view);
//addView后一定要measure,先measure再layout
measureChildWithMargins(view, 0, 0);
layoutDecorated(view, rect.left, rect.top, rect.right, rect.bottom);
}
//如果所有子View的高度和沒(méi)有填滿RecyclerView的高度,
// 則將高度設(shè)置為RecyclerView的高度
mTotalHeight = Math.max(offsetY, getVerticalSpace());
}
@Override
public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
if (getChildCount() <= 0) {
return dy;
}
int travel = dy;
//如果滑動(dòng)到最頂部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑動(dòng)到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
mSumDy += travel;
Rect visibleRect = getVisibleArea();
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
int position = getPosition(child);
Rect rect = mItemRects.get(position);
if (!Rect.intersects(rect, visibleRect)) {
removeAndRecycleView(child, recycler);
mHasAttachedItems.put(position,false);
} else {
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
child.setRotationY(child.getRotationY() + 1);
mHasAttachedItems.put(position, true);
}
}
View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
if (travel >= 0) {
int minPos = getPosition(firstView);
for (int i = minPos; i < getItemCount(); i++) {
insertView(i, visibleRect, recycler, false);
}
} else {
int maxPos = getPosition(lastView);
for (int i = maxPos; i >= 0; i--) {
insertView(i, visibleRect, recycler, true);
}
}
return travel;
}
private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
Rect rect = mItemRects.get(pos);
if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
View child = recycler.getViewForPosition(pos);
if (firstPos) {
addView(child, 0);
} else {
addView(child);
}
measureChildWithMargins(child, 0, 0);
layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
//在布局item后,修改每個(gè)item的旋轉(zhuǎn)度數(shù)
child.setRotationY(child.getRotationY() + 1);
mHasAttachedItems.put(pos,true);
}
}