自定義LayoutManager之復(fù)用與回收二

轉(zhuǎn)自RecyclerView系列之五回收復(fù)用實(shí)現(xiàn)方式二
自定義LayoutManager之復(fù)用與回收一,我們已經(jīng)實(shí)現(xiàn)了自定義LayoutManager的復(fù)用與回收,但是我們直接調(diào)用了offsetChildrenVertical(-travel)來(lái)實(shí)現(xiàn)了item的滾動(dòng),這個(gè)方法僅適用于每個(gè)item在移動(dòng)時(shí)沒(méi)什么特殊的情況,當(dāng)在滑動(dòng)時(shí)需要修改每個(gè)item的角度、透明度等情況時(shí),單純使用offsetChildrenVertical(-travel)是不可行的。針對(duì)這種情況,本文介紹實(shí)現(xiàn)復(fù)用回收的第二種方式。
在本節(jié)中,我們最終實(shí)現(xiàn)的效果如下圖所示:

20181212203728536.gif

從效果中可以看出,在滑動(dòng)過(guò)程中同時(shí)每個(gè)item繞y軸旋轉(zhuǎn),因?yàn)榇蟛糠衷砗蜕衔闹械腃ustomLayoutManager相同,只需在上文的基礎(chǔ)上進(jìn)行修改即可。

1、初步實(shí)現(xiàn)

1.1、實(shí)現(xiàn)原理

在這里,我們需要去掉offsetChildrenVertical(-travel)滑動(dòng)item,然后自己去布局每個(gè)item。很明顯,我們只需要處理滑動(dòng),所以onLayoutChildren初始化布局邏輯不需修改,只需要修改scrollVerticallyBy()方法中邏輯。
在滑動(dòng)過(guò)程中,有兩種item需要重新布局:

  • 第一種:原來(lái)已經(jīng)在屏幕中的item
  • 第二種:新增的item

所以這里就涉及到如何處理已經(jīng)在屏幕上的item和新增item的重繪問(wèn)題,這里可以效仿onLayoutChildren方法的實(shí)現(xiàn)方式,先調(diào)用detachAndScrapAttachedViews(recycler)方法分離屏幕上的item,然后再重繪所有item。

那么應(yīng)該重繪哪些item呢?這里依然分兩種情況:

  • 1、當(dāng)向上滾動(dòng)時(shí),頂部item向上移動(dòng),底部空出空白,所以我們只需從當(dāng)前顯示的第一個(gè)item向下遍歷直到結(jié)束。
  • 2、當(dāng)向下滾動(dòng)時(shí),底部item向下移動(dòng),頂部留出空白,此時(shí)只需要從當(dāng)前顯示的最后一個(gè)item向上遍歷,直接index=0為止。

1.2、改造CustomLayoutManager

上面已經(jīng)說(shuō)了,只需要修改scrollVerticallyBy()中邏輯即可。其中頂部、底部的邊界判斷,以及回收的邏輯不需要修改。

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() <= 0) {
        return dy;
    }

    int travel = dy;
    //如果滑動(dòng)到最頂部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        //如果滑動(dòng)到最底部
        travel = mTotalHeight - getVerticalSpace() - mSumDy;
    }

    //回收越界子View
    for (int i = getChildCount() - 1; i >= 0; i--) {
        View child = getChildAt(i);
        if (travel > 0) {//需要回收當(dāng)前屏幕,上越界的View
            if (getDecoratedBottom(child) - travel < 0) {
                removeAndRecycleView(child, recycler);
                continue;
            }
        } else if (travel < 0) {//回收當(dāng)前屏幕,下越界的View
            if (getDecoratedTop(child) - travel > getHeight() - getPaddingBottom()) {
                removeAndRecycleView(child, recycler);
                continue;
            }
        }
    }
    …………
}

在回收之后,調(diào)用detachAndScrapAttachedViews(recycler);將屏幕上可見(jiàn)的item進(jìn)行剝離,在剝離之前需要先記錄當(dāng)前屏幕顯示的第一個(gè)item和最后一個(gè)item的索引,否則在調(diào)用detachAndScrapAttachedViews(recycler);之后,調(diào)用getChildAt(i)就會(huì)返回null。

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

這里需要注意的是,我們?cè)谒胁季植僮髦埃葘⒁苿?dòng)距離進(jìn)行累加。因?yàn)楹竺嫖覀冊(cè)诓季謎tem時(shí),會(huì)棄用offsetChildrenVertical(-travel)移動(dòng)item,而在布局時(shí)直接將item布局在新位置。最后,因?yàn)槲覀円呀?jīng)累加了mSumDy,所以我們需要改造getVisibleArea(),將原來(lái)getVisibleArea(int dy)中累加dy的操作去掉:

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

接下來(lái),就是布局屏幕上的所有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)條件時(shí)直接break。比如在向上滑動(dòng)travel前,當(dāng)前屏幕上有三個(gè)可見(jiàn)的item且此時(shí)第一個(gè)item馬上要滑出屏幕,在向上滑動(dòng)travel時(shí),第一個(gè)item不在屏幕內(nèi)了,此時(shí)會(huì)執(zhí)行注釋處的else代碼執(zhí)行break,后面可見(jiàn)的item將不能布局在屏幕上,由于在布局前調(diào)用了detachAndScrapAttachedViews(recycler)剝離了item,所以此時(shí)整個(gè)屏幕一片空白。

所以當(dāng)travel>0表示向上滑動(dòng),就需要從當(dāng)前顯示的第一個(gè)item開始遍歷,由于我們不知道到哪里結(jié)束,所以就是用最后一個(gè)item的索引(getItemCount)作為結(jié)束位置。
當(dāng)然大家在這里也可以優(yōu)化,可以使用下面的語(yǔ)句:

int max = minPos + 50 < getItemCount() ? minPos + 50 : getItemCount();

即從第一個(gè)item向后累加50項(xiàng),如果最后的索引小于getItemCount(),就用minPos + 50作為結(jié)束值,否則用getItemCount()作為結(jié)束值。這里的50并不是固定的,可以根據(jù)實(shí)際情況進(jìn)行修改。

然后在dy<0時(shí),表示向下滾動(dòng)

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

因?yàn)槭窍蛳聺L動(dòng),所以頂部新增,底部回收,所以我們需要從當(dāng)前底部可見(jiàn)的最后一個(gè)item向上遍歷,將每個(gè)item布局到新位置,但什么時(shí)候截止呢?我們同樣可以向上減50:

int min = maxPos - 50 >= 0 ? maxPos - 50 : 0;

這里我為了方便理解,還是一直遍歷到索引0;

代碼到這里就改造完了,scrollVerticallyBy的核心代碼如下(除去到頂、頂?shù)着袛嗪驮浇缁厥眨?/p>

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時(shí),調(diào)用child.setRotationY(child.getRotationY()+1);將它的圍繞Y軸的旋轉(zhuǎn)度數(shù)加1,所以每滾動(dòng)一次,就會(huì)旋轉(zhuǎn)度數(shù)加1.這樣就實(shí)現(xiàn)了開篇的效果了。

2、繼續(xù)優(yōu)化:回收時(shí)布局

在上部分中,我們通過(guò)先使用detachAndScrapAttachedViews(recycler)將所有item離屏緩存,然后通過(guò)再重新布局所有item的方法來(lái)實(shí)現(xiàn)回收復(fù)用。

但是這里有個(gè)問(wèn)題,我們能不能把已經(jīng)在屏幕上的item直接布局呢?這樣就省去了先離屏緩存再重新布局的操作,提高了性能。

那這個(gè)直接布局已經(jīng)在屏幕上的item的步驟,放在哪里呢?我們知道,我們?cè)诨厥赵浇鏸tem時(shí),會(huì)遍歷所有的可見(jiàn)item,所以我們可以把它放在回收越界時(shí),如果越界就回收,如果沒(méi)越界就重新布局:

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

因?yàn)楹竺嫖覀冞€需要布局所有Item,很明顯,在全部布局時(shí),這些已經(jīng)布局過(guò)的item就需要排除掉,所以我們需要一個(gè)變量來(lái)保存在這里哪些item已經(jīng)布局好了:

所以,我們先申請(qǐng)一個(gè)成員變量:

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時(shí),把所有item所對(duì)應(yīng)的值設(shè)置為false,表示所有item都沒(méi)有被重新布局。

然后在回收越界holdview時(shí),將已經(jīng)重新布局的item置為true.將被回收的item,回收時(shí)設(shè)置為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時(shí),添加判斷當(dāng)前的item是否已經(jīng)被布局,沒(méi)布局的item再布局,需要注意的是,在布局后,需要將mHasAttachedItems中對(duì)應(yīng)位置改為true,表示已經(jīng)在布局中了。

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) {//沒(méi)有Item,界面空著吧
        detachAndScrapAttachedViews(recycler);
        return;
    }
    mHasAttachedItems.clear();
    mItemRects.clear();

    detachAndScrapAttachedViews(recycler);

    //將item的位置存儲(chǔ)起來(lái)
    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的高度和沒(méi)有填滿RecyclerView的高度,
    // 則將高度設(shè)置為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;
    //如果滑動(dòng)到最頂部
    if (mSumDy + dy < 0) {
        travel = -mSumDy;
    } else if (mSumDy + dy > mTotalHeight - getVerticalSpace()) {
        //如果滑動(dòng)到最底部
        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后,修改每個(gè)item的旋轉(zhuǎn)度數(shù)
        child.setRotationY(child.getRotationY() + 1);
        mHasAttachedItems.put(pos,true);
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,081評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,025評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,204評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,461評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,945評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,205評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容