自定义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);
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容