Android 回收复用RecyclerView实现方式二

2020-06-21  本文已影响0人  as_pixar

先相信你自己,然后别人才会相信你。——屠格涅夫

上篇博客 https://www.jianshu.com/p/8509e0228386

在上篇中,我们先将摆好所有要显示的新增item以后,再使用offsetChildrenVertical(-travel)函数来移动屏幕中所有item。很明显,这种方法仅适用于每个item,在移动时,没有特殊效果的情况,当我们在移动item时,同时需要改变item的角度、透明度等情况时,单纯使用offsetChildrenVertical(-travel)来移是不行的。针对这种情况,我们就只有使用第二种方法来实现回收复用了。

在本节中,我们最终实现的效果如下图所示:


从效果图中可以看出,本例中的每个Item,在移动时,同时会绕X轴旋转。

因为大部分的原理与上节中的CustomLayoutManager的实现相同,所以本节中的代码将从CustomLayoutManager中改造而成。

实现原理

在这里,我们主要替换掉在上节中移动item所用的offsetChildrenVertical(-travel);函数,既然要将它弃用,那我们就只能自己布局每个item了。很明显,在这里我们主要处理的是滚动的情况,对于onLayoutChildren中的代码是不用改动的。

试想,在滚动dy时,有两种item需要重新布局:
第一种:原来已经在屏幕上的item
第二种:新增的item

所以,这里就涉及到怎么处理已经在屏幕上的item和新增item的重绘问题,我们可以效仿在onLayoutChildren中的处理方式,先调用detachAndScrapAttachedViews(recycler)将屏幕上已经在显示的所有Item离屏,然后再将所有item重绘。

那第二个问题又来了,我们应该从哪个item开始重绘,到哪个item结束呢?

很明显,在向下滚动时,底部Item下移,顶部空出来空白区域。所以我们只需要从当前在显示的Item向前遍历,直到index=0即可。
当向上滚动时,顶部Item上移,底部空出来空白区域。所以我们也只需要从当前在显示的顶部Item向上遍历,直到Item结束为止。

改造CustomLayoutManager

首先,onLayoutChildren不用改造,只需要改造scrollVerticallyBy即可。原来的到顶、到底判断和回收越界Item的代码都不变:

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;
            }
        }
    }
    …………
}

在回收越界的HolderView之后,我们需要在使用detachAndScrapAttachedViews(recycler);将现在显示的所有item离屏缓存之前,先得到当前在显示的第一个item和最后一个item的索引,因为如果在将所有item从屏幕上离屏缓存以后,利用getChildAt(int position)是拿不到任何值的,会返回null,因为现在屏幕上已经没有View存在了。

View lastView = getChildAt(getChildCount() - 1);
View firstView = getChildAt(0);
detachAndScrapAttachedViews(recycler);
mSumDy += travel;
Rect visibleRect = getVisibleArea();

这里需要注意的是,我们在所有的布局操作前,先将移动距离mSumDy进行了累加。因为后面我们在布局item时,会弃用offsetChildrenVertical(-travel)移动item,而是在布局item时,就直接把item布局在新位置。最后,因为我们已经累加了travel,所以我们需要改造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);
        }
    }
} 

这里需要注意的是,当dy>0时,表示向上滚动(手指由下向上滑),所以我们需要从之前第一个可见的item向下遍历,因为我们不知道在什么情况下遍历结束,所以我们使用最后一个item的索引(getItemCount())做为结束位置。

然后在在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布局到新位置。

代码到这里就改造完了,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那块非常冗余,在travel>=0时和travel<0时,要写两遍,除了插入位置不同以外,其它都完全相同的,所以我们可以抽出来一个函数来做addView的事情:

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++) {
            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)) {
        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.setRotationX(child.getRotationX() + 1);
    }
}

在这里将布局时,用到的公共部分抽出来一个函数,命名为insertView,在这个函数中,我们先将这个item布局,然后在布局后,调用child.setRotationX(child.getRotationX() + 1);;将它的围绕X轴的旋转度数加1,所以每滚动一次,就会旋转度数加1.这样就实现了开篇的效果了。
我们贴出完整代码

private Rect getVisibleArea() {
        Rect result = new Rect(getPaddingLeft(), getPaddingTop() + mSumDy, getWidth() + getPaddingRight(), getVerticalSpace() + mSumDy);
        return result;
    }

    private int getVerticalSpace() {
        return getHeight() - getPaddingBottom() - getPaddingTop();
    }

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    private int mSumDy = 0;

    @Override
    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;
                }
            }
        }

        View lastView = getChildAt(getChildCount() - 1);
        View firstView = getChildAt(0);
        detachAndScrapAttachedViews(recycler);
        mSumDy += travel;
        Rect visibleRect = getVisibleArea();

        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, RecyclerView.Recycler recycler, boolean firstPos) {
        Rect rect = mItemRects.get(pos);
        if (Rect.intersects(visibleRect, rect)) {
            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.setRotationX(child.getRotationX() + 1);
        }
    }

再看日志的复用情况:

06-21 08:21:45.336 5038-5038/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:6
06-21 08:21:45.338 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:45.363 5038-5038/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:7
06-21 08:21:45.366 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:48.044 5038-5038/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:8
06-21 08:21:48.047 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:48.382 5038-5038/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:9
06-21 08:21:48.384 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:48.554 5038-5038/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:10
06-21 08:21:48.556 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:49.373 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:49.533 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:49.911 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:50.938 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:51.400 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:21:52.029 5038-5038/com.example.myrecyclerview D/TAG: --------onBindViewHolder

可以看到回收复用情况不变,这就初步实现了布局每个item的改造,下面我们继续对它进行优化。

继续优化:回收时布局

在上部分中,我们通过先使用detachAndScrapAttachedViews(recycler)将所有item离屏缓存,再重新布局所有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.setRotationX(child.getRotationX() + 1);
    }
}

因为后面我们还需要布局所有Item,很明显,在全部布局时,这些已经布局过的item就需要排除掉,所以我们需要一个变量来保存哪些Item已经布局好了,所以,我们先申请一个成员变量:

private SparseBooleanArray mHasAttachedItems = new SparseBooleanArray();

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.setRotationX(child.getRotationX() + 1);
            mHasAttachedItems.put(i, true);
        }
    }
    …………
}

最后在布局所有Item时,添加判断当前的Item是否已经被布局,没布局的Item再布局,需要注意的是,在布局后,需要将mHasAttachedItems中对应位置改为true,表示已经在布局中了。

private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
    Rect rect = mItemRects.get(pos);
    if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
        …………
        layoutDecoratedWithMargins(child, rect.left, rect.top - mSumDy, rect.right, rect.bottom - mSumDy);
        child.setRotationX(child.getRotationX() + 1);
        mHasAttachedItems.put(pos,true);
    }
}

最后一步,最关键的,不要忘了删除scrollVerticallyBy中的detachAndScrapAttachedViews(recycler);
完整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.setRotationX(child.getRotationX() + 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.setRotationX(child.getRotationX() + 1);
        mHasAttachedItems.put(pos,true);
    }
}

我们打印日志看看不用情况

06-21 08:46:11.135 5495-5495/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:5
06-21 08:46:11.136 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:11.136 5495-5495/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:6
06-21 08:46:11.137 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:11.250 5495-5495/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:7
06-21 08:46:11.253 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:13.021 5495-5495/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:8
06-21 08:46:13.023 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:13.310 5495-5495/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:9
06-21 08:46:13.313 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:13.635 5495-5495/com.example.myrecyclerview D/TAG: -----onCreateViewHolder  num:10
06-21 08:46:13.637 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:14.477 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:14.586 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:14.734 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:15.960 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:16.056 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:16.271 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:16.683 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:16.935 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:17.268 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder
06-21 08:46:17.426 5495-5495/com.example.myrecyclerview D/TAG: --------onBindViewHolder

到这里,自定义LayoutManager的部分就结束了,这两节中,我们主要讲解了一般情况下的回收复用方法和本节的特殊情况下的回收复用方法,不过一般对于优秀特效而言,本节布局回收每个item的方法用的最多。

上一篇下一篇

猜你喜欢

热点阅读