当前位置 博文首页 > 启舰:自定义控件三部曲视图篇(八)——RecyclerView系列之五回

    启舰:自定义控件三部曲视图篇(八)——RecyclerView系列之五回

    作者:[db:作者] 时间:2021-06-30 12:41

    前言 只要有坚强的持久心,一个庸俗平凡的人也会有成功的一天,否则即使是一个才识卓越的人,也只能遭遇失败的命运。 -----比尔盖茨


    系列文章: Android自定义控件三部曲文章索引: http://blog.csdn.net/harvic880925/article/details/50995268


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

    在本节中,我们最终实现的效果如下图所示:
    在这里插入图片描述

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

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

    一、 初步实现

    1.1 实现原理

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

    试想,在滚动dy时,有两种item需要重新布局:

    1. 第一种:原来已经在屏幕上的item
    2. 第二种:新增的item

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

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

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

    1.2 改造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布局在新位置。最后,因为我们已经累加了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);
            }
        }
    } 
    

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

    public int scrollVerticallyBy(