转自RecyclerView系列之五回收复用实现方式二
在自定义LayoutManager之复用与回收一,我们已经实现了自定义LayoutManager的复用与回收,但是我们直接调用了offsetChildrenVertical(-travel)
来实现了item的滚动,这个方法仅适用于每个item在移动时没什么特殊的情况,当在滑动时需要修改每个item的角度、透明度等情况时,单纯使用offsetChildrenVertical(-travel)
是不可行的。针对这种情况,本文介绍实现复用回收的第二种方式。
在本节中,我们最终实现的效果如下图所示:
从效果中可以看出,在滑动过程中同时每个item绕y轴旋转,因为大部分原理和上文中的CustomLayoutManager相同,只需在上文的基础上进行修改即可。
1、初步实现
1.1、实现原理
在这里,我们需要去掉offsetChildrenVertical(-travel)
滑动item,然后自己去布局每个item。很明显,我们只需要处理滑动,所以onLayoutChildren初始化布局逻辑不需修改,只需要修改scrollVerticallyBy()
方法中逻辑。
在滑动过程中,有两种item需要重新布局:
- 第一种:原来已经在屏幕中的item
- 第二种:新增的item
所以这里就涉及到如何处理已经在屏幕上的item和新增item的重绘问题,这里可以效仿onLayoutChildren方法的实现方式,先调用detachAndScrapAttachedViews(recycler)
方法分离屏幕上的item,然后再重绘所有item。
那么应该重绘哪些item呢?这里依然分两种情况:
- 1、当向上滚动时,顶部item向上移动,底部空出空白,所以我们只需从当前显示的第一个item向下遍历直到结束。
- 2、当向下滚动时,底部item向下移动,顶部留出空白,此时只需要从当前显示的最后一个item向上遍历,直接index=0为止。
1.2、改造CustomLayoutManager
上面已经说了,只需要修改scrollVerticallyBy()
中逻辑即可。其中顶部、底部的边界判断,以及回收的逻辑不需要修改。
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() <= 0) {
return dy;
}
int travel = dy;
//如果滑动到最顶部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑动到最底部
travel = mTotalHeight - getVerticalSpace() - mSumDy;
}
//回收越界子View
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (travel > 0) {//需要回收当前屏幕,上越界的View
if (getDecoratedBottom(child) - travel < 0) {
removeAndRecycleView(child, recycler);
continue;
}
} else if (travel < 0) {//回收当前屏幕,下越界的View
if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
removeAndRecycleView(child, recycler);
continue;
}
}
}
…………
}
在回收之后,调用detachAndScrapAttachedViews(recycler);
将屏幕上可见的item进行剥离,在剥离之前需要先记录当前屏幕显示的第一个item和最后一个item的索引,否则在调用detachAndScrapAttachedViews(recycler);
之后,调用getChildAt(i)
就会返回null。
View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
detachAndScrapAttachedViews(recycler);
mSumDy += travel;
Rect visibleRect = getVisibleArea();
这里需要注意的是,我们在所有布局操作之前,先将移动距离进行累加。因为后面我们在布局item时,会弃用offsetChildrenVertical(-travel)
移动item,而在布局时直接将item布局在新位置。最后,因为我们已经累加了mSumDy,所以我们需要改造getVisibleArea(),将原来getVisibleArea(int dy)中累加dy的操作去掉:
private Rect getVisibleArea() {
Rect result = new Rect(getPaddingLeft(), getPaddingTop() + mSumDy, getWidth() + getPaddingRight(), getVerticalSpace() + mSumDy);
return result;
}
接下来,就是布局屏幕上的所有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)
条件时直接break。比如在向上滑动travel前,当前屏幕上有三个可见的item且此时第一个item马上要滑出屏幕,在向上滑动travel时,第一个item不在屏幕内了,此时会执行注释处的else代码执行break,后面可见的item将不能布局在屏幕上,由于在布局前调用了detachAndScrapAttachedViews(recycler)
剥离了item,所以此时整个屏幕一片空白。
所以当travel>0表示向上滑动,就需要从当前显示的第一个item开始遍历,由于我们不知道到哪里结束,所以就是用最后一个item的索引(getItemCount
)作为结束位置。
当然大家在这里也可以优化,可以使用下面的语句:
int max = minPos + 50 < getItemCount() ? minPos + 50 : getItemCount();
即从第一个item向后累加50项,如果最后的索引小于getItemCount()
,就用minPos + 50
作为结束值,否则用getItemCount()
作为结束值。这里的50并不是固定的,可以根据实际情况进行修改。
然后在dy<0时,表示向下滚动
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);
}
}
}
因为是向下滚动,所以顶部新增,底部回收,所以我们需要从当前底部可见的最后一个item向上遍历,将每个item布局到新位置,但什么时候截止呢?我们同样可以向上减50:
int min = maxPos - 50 >= 0 ? maxPos - 50 : 0;
这里我为了方便理解,还是一直遍历到索引0;
代码到这里就改造完了,scrollVerticallyBy的核心代码如下(除去到顶、顶底判断和越界回收)
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时,调用child.setRotationY(child.getRotationY()+1);
将它的围绕Y轴的旋转度数加1,所以每滚动一次,就会旋转度数加1.这样就实现了开篇的效果了。
2、继续优化:回收时布局
在上部分中,我们通过先使用detachAndScrapAttachedViews(recycler)
将所有item离屏缓存,然后通过再重新布局所有item的方法来实现回收复用。
但是这里有个问题,我们能不能把已经在屏幕上的item直接布局呢?这样就省去了先离屏缓存再重新布局的操作,提高了性能。
那这个直接布局已经在屏幕上的item的步骤,放在哪里呢?我们知道,我们在回收越界item时,会遍历所有的可见item,所以我们可以把它放在回收越界时,如果越界就回收,如果没越界就重新布局:
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);
}
}
因为后面我们还需要布局所有Item,很明显,在全部布局时,这些已经布局过的item就需要排除掉,所以我们需要一个变量来保存在这里哪些item已经布局好了:
所以,我们先申请一个成员变量:
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时,把所有item所对应的值设置为false,表示所有item都没有被重新布局。
然后在回收越界holdview时,将已经重新布局的item置为true.将被回收的item,回收时设置为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时,添加判断当前的item是否已经被布局,没布局的item再布局,需要注意的是,在布局后,需要将mHasAttachedItems中对应位置改为true,表示已经在布局中了。
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) {//没有Item,界面空着吧
detachAndScrapAttachedViews(recycler);
return;
}
mHasAttachedItems.clear();
mItemRects.clear();
detachAndScrapAttachedViews(recycler);
//将item的位置存储起来
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的高度和没有填满RecyclerView的高度,
// 则将高度设置为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;
//如果滑动到最顶部
if (mSumDy + dy < 0) {
travel = -mSumDy;
} else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
//如果滑动到最底部
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后,修改每个item的旋转度数
child.setRotationY(child.getRotationY() + 1);
mHasAttachedItems.put(pos,true);
}
}