自定义LayoutManager之复用与回收二

转自RecyclerView系列之五回收复用实现方式二
在自定义LayoutManager之复用与回收一,我们已经实现了自定义LayoutManager的复用与回收,但是我们直接调用了offsetChildrenVertical(-travel)来实现了item的滚动,这个方法仅适用于每个item在移动时没什么特殊的情况,当在滑动时需要修改每个item的角度、透明度等情况时,单纯使用offsetChildrenVertical(-travel)是不可行的。针对这种情况,本文介绍实现复用回收的第二种方式。
在本节中,我们最终实现的效果如下图所示:

20181212203728536.gif

从效果中可以看出,在滑动过程中同时每个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);
    }
}

你可能感兴趣的:(自定义LayoutManager之复用与回收二)