自定義控件三部曲視圖篇(八)——RecyclerView系列之五回收複用實現方式二

前言 只要有堅強的持久心,一個庸俗平凡的人也會有成功的一天,否則即使是一個才識卓越的人,也只能遭遇失敗的命運。 -----比爾蓋茨


系列文章: 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(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.setRotationY(child.getRotationY()+1);
    }
}

在這裏將佈局時,用到的公共部分抽出來一個函數,命名爲insertView,在這個函數中,我們先將這個item佈局,然後在佈局後,調用child.setRotationY(child.getRotationY()+1);將它的圍繞Y軸的旋轉度數加1,所以每滾動一次,就會旋轉度數加1.這樣就實現了開篇的效果了。
在這裏插入圖片描述

再看日誌的複用情況:

在這裏插入圖片描述

可以看到回收複用情況不變,這就初步實現了佈局每個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.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,表示已經在佈局中了。

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.setRotationY(child.getRotationY() + 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.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);
    }
}

此時,大家去打日誌來看回收復用情況,也跟LinearLayoutManager是完全相同的,這裏就不再截圖了。

到這裏,自定義LayoutManager的部分就結束了,這兩節中,我們主要講解了一般情況下的回收複用方法和本節的特殊情況下的回收複用方法,不過一般對於優秀特效而言,本節佈局回收每個item的方法用的最多。


源碼在文章底部給出


如果我的文章對你有幫助的話,你應該也會喜歡我的公衆號
在這裏插入圖片描述


如果本文有幫到你,記得加關注哦
CSDN源碼現在不能零分下載了,必須強制最低一分,我設置爲了最低分,如果沒分的同學,可以從github上下載。
源碼地址:https://download.csdn.net/download/harvic880925/10846534
github代碼地址:https://github.com/harvic/harvic_blg_share 位於RecylcerView(五)
轉載請標明出處,https://blog.csdn.net/harvic880925/article/details/84979161 謝謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章