自定義LayoutManager實(shí)現(xiàn)滾動(dòng)畫(huà)廊控件

轉(zhuǎn)自RecycerView系列之六實(shí)現(xiàn)滾動(dòng)畫(huà)廊控件

1、滾動(dòng)畫(huà)廊控件

本節(jié)實(shí)現(xiàn)的效果如下圖所示:


滾動(dòng)畫(huà)廊.gif

2儒洛、實(shí)現(xiàn)橫向布局

2.1精耐、開(kāi)啟橫向滾動(dòng)

自定義LayoutManager之復(fù)用與回收二中已經(jīng)介紹了如何通過(guò)自定義LayoutManager實(shí)現(xiàn)垂直滾動(dòng)的效果,由于本文中效果是中橫向滾動(dòng)的琅锻,故在此基礎(chǔ)上進(jìn)行修改卦停。
首先刪除canScrollVertically()scrollVerticallyBy函數(shù),改為:

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

@Override
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
    …………
}

在經(jīng)過(guò)上面修改后恼蓬,可以成功運(yùn)行惊完,但是布局依然是豎直布局的,很明顯需要修改onLayoutChildren()進(jìn)行橫向布局处硬。

2.2小槐、實(shí)現(xiàn)橫向布局

最關(guān)鍵的問(wèn)題就是,我們?cè)诔跏蓟季謺r(shí)荷辕,會(huì)通過(guò)mItemRects來(lái)保存所有item的位置凿跳,所以這里需要修改成橫向布局的計(jì)算方式。

int offsetX = 0;

for (int i = 0; i < getItemCount(); i++) {
    Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
    mItemRects.put(i, rect);
    mHasAttachedItems.put(i, false);
    offsetX += mItemWidth;
}

然后在獲取visibleCount時(shí)骡显,需要修改為:

int visibleCount = getHorizontalSpace() / mItemWidth;

同時(shí)石挂,在onLayoutChildren最后窖式,有個(gè)計(jì)算mTotalHeight的邏輯阁簸,我們需要改為計(jì)算totalWidth的邏輯:

@Override
public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {
    …………
    mTotalWidth = Math.max(offsetX, getHorizontalSpace());
}

private int getHorizontalSpace() {
    return getWidth() - getPaddingLeft() - getPaddingRight();
}

同時(shí)桨啃,在getVisibleArea函數(shù)也需要修改羹饰,因?yàn)槲覀儸F(xiàn)在已經(jīng)是橫向滾動(dòng)了筒主,已經(jīng)不再是豎向滾動(dòng)了,所以可見(jiàn)區(qū)域應(yīng)該是橫向滾動(dòng)后的可見(jiàn)區(qū)域:

private Rect getVisibleArea() {
    Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
    return result;
}

onLayoutChildren函數(shù)中的其它代碼不需要更改,此時(shí)onLayoutChildren的代碼如下:

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() / mItemWidth;


    //定義水平方向的偏移量
    int offsetX = 0;

    for (int i = 0; i < getItemCount(); i++) {
        Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
        mItemRects.put(i, rect);
        mHasAttachedItems.put(i, false);
        offsetX += mItemWidth;
    }

    Rect visibleRect = getVisibleArea();
    for (int i = 0; i < visibleCount; i++) {
        insertView(i, visibleRect, recycler, false);
    }

    //如果所有子View的寬度和沒(méi)有填滿RecyclerView的寬度沃于,
    // 則將寬度設(shè)置為RecyclerView的寬度
    mTotalWidth = Math.max(offsetX, getHorizontalSpace());
}

private int getHorizontalSpace() {
    return getWidth() - getPaddingLeft() - getPaddingRight();
}

private Rect getVisibleArea() {
    Rect result = new Rect(getPaddingLeft() + mSumDx, getPaddingTop(), getWidth() - getPaddingRight() + mSumDx, getHeight()-getPaddingBottom());
    return result;
}

修改后的效果如下圖所示:


image.png

到此已經(jīng)實(shí)現(xiàn)了橫向的布局薄风,下面修改下橫向滾動(dòng)的邏輯。

2.3锌畸、實(shí)現(xiàn)橫向滾動(dòng)

橫向滾動(dòng)是放在scrollHorizontallyBy中處理粘咖,主要的修改如下:

  • 1例证、邊界處理修改
 int travel = dx;
    //如果滑動(dòng)到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
        //如果滑動(dòng)到最底部
        travel = mTotalWidth - getHorizontalSpace() - mSumDx;
    }

邊界的處理和垂直的處理邏輯大致相同捅位,只需要進(jìn)行簡(jiǎn)單的修改姜胖。

  • 2、在回收越界時(shí)棍现,已經(jīng)在屏幕上的item重新Layout的修改:
//回收越界子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 - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
        mHasAttachedItems.put(position, true);
    }
}

這里只需要修改layoutDecoratedWithMargins函數(shù)即可般甲,在布局時(shí),根據(jù)mSumDx布局item的left和right坐標(biāo):layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);,因?yàn)槭菣M向布局,所以top和bottom都不變旷痕。

  • 3志鹃、在移動(dòng)后需要處理空白區(qū)域的填充,同樣涉及到layout操作淳玩,故需要處理承匣。
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 - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);

        mHasAttachedItems.put(pos, true);
    }
}

到此就實(shí)現(xiàn)了橫向的滾動(dòng)效果了些侍,效果如下:


橫向滾動(dòng).gif

完整的scrollHorizontallyBy代碼如下

public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
    if (getChildCount() <= 0) {
        return dx;
    }

    int travel = dx;
    //如果滑動(dòng)到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
        //如果滑動(dòng)到最底部
        travel = mTotalWidth - getHorizontalSpace() - mSumDx;
    }

    mSumDx += 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 - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
            mHasAttachedItems.put(position, true);
        }
    }

    //填充空白區(qū)域
    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 - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);

        mHasAttachedItems.put(pos, true);
    }
}

2.4刊咳、實(shí)現(xiàn)卡片疊加

從最終的效果圖中可以看出捕犬,我們兩個(gè)卡片之間并不是并排排列的,而是疊加在一起的。在這個(gè)例子中斩跌,兩個(gè)卡片之間疊加的部分是半個(gè)卡片的大小。所以嗅虏,我們需要修改排列卡片的代碼,使卡片疊加起來(lái)。

首先塘娶,申請(qǐng)一個(gè)變量,保存兩個(gè)卡片之間的距離:

private int mIntervalWidth;

private int getIntervalWidth() {
    return mItemWidth / 2;
}

然后在onLayoutChildren中酝碳,首先給mIntervalWidth初始化,然后在計(jì)算每個(gè)卡片的起始位置時(shí)芽偏,offsetX每次位移距離,改為offsetX += mIntervalWidth,具體代碼如下:

mIntervalWidth = getIntervalWidth();

//定義水平方向的偏移量
int offsetX = 0;

for (int i = 0; i < getItemCount(); i++) {
    Rect rect = new Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight);
    mItemRects.put(i, rect);
    mHasAttachedItems.put(i, false);
    offsetX += mIntervalWidth;
}

這里需要注意的是缩抡,在計(jì)算每個(gè)卡片的位置時(shí)Rect(offsetX, 0, offsetX + mItemWidth, mItemHeight),在這個(gè)Rect的right位置贵少,不能改為offsetX + mIntervalWidth,因?yàn)槲覀冎皇歉牧丝ㄆ季謺r(shí)的起始位置滔灶,并沒(méi)有更改卡片的大小普碎,所以每個(gè)卡片的長(zhǎng)度和寬度是不能變的。

然后在初始化時(shí)插入item時(shí)录平,在計(jì)算visibleCount時(shí),需要改為int visibleCount = getHorizontalSpace() / mIntervalWidth,代碼如下:

int visibleCount = getHorizontalSpace() / mIntervalWidth;
Rect visibleRect = getVisibleArea();
for (int i = 0; i < visibleCount; i++) {
    insertView(i, visibleRect, recycler, false);
}

因?yàn)樵趕crollHorizontallyBy中處理滾動(dòng)時(shí)氛琢,每個(gè)卡片的位置都是直接從mItemRects中取的逗嫡,所以念秧,我們并不需要在修改滾動(dòng)時(shí)的代碼。

到這里留荔,就實(shí)現(xiàn)了卡片疊加的功能辞州,效果如下圖所示:


卡片疊加.gif

2.5派殷、修改卡片的起始位置

到現(xiàn)在,我們卡片都還是在最左側(cè)開(kāi)始展示的宗挥,但在開(kāi)篇的效果圖中可以看出,在初始化時(shí)交排,第一個(gè)item是在最屏幕中間顯示的划滋,這是怎么做到的呢?

首先埃篓,我們需要先申請(qǐng)一個(gè)變量mStartX处坪,來(lái)保存卡片后移的距離。

很明顯都许,這里也只是改變每個(gè)卡片的布局位置稻薇,所以我們也只需要在onLayoutChildren中,在mItemRects中初始化每個(gè)item位置時(shí)胶征,將每個(gè)item后移mStartX就可以了。

所以核心代碼如下:

private int mStartX;

public void onLayoutChildren(Recycler recycler, RecyclerView.State state) {

    …………
    mStartX = getWidth()/2 - mIntervalWidth;

    //定義水平方向的偏移量
    int offsetX = 0;
    for (int i = 0; i < getItemCount(); i++) {
        Rect rect = new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight);
        mItemRects.put(i, rect);
        mHasAttachedItems.put(i, false);
        offsetX += mIntervalWidth;
    }
    …………
}

首先桨仿,是mStartX的初始化睛低,因?yàn)槲覀冃枰谝粋€(gè)卡片的中間位置在屏幕正中間的位置,從下圖中明顯可以看出姥芥,mStartX的值應(yīng)該是:mStartX = getWidth()/2 - mIntervalWidth;

image.png

然后仍稀,在計(jì)算每個(gè)item的rect時(shí)藏姐,將每個(gè)item后移mStartX距離:new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight)

就這樣摆出,我們就完成了移動(dòng)初始化位置的功能宅荤,效果如下圖所示:


修改初始位置.gif

2.6席函、更改默認(rèn)顯示順序

2.6.1求摇、 更改默認(rèn)顯示順序的原理

現(xiàn)在峰鄙,我們每個(gè)item的顯示順序還是后一個(gè)卡片壓在前一個(gè)卡片上顯示的灿椅,這是因?yàn)樘椎伲赗ecyclerView繪制時(shí),先繪制的第一個(gè)item茫蛹,然后再繪制第二個(gè)item操刀,然后再繪制第三個(gè)item,……,默認(rèn)就是這樣的繪制順序婴洼。即越往前的item越優(yōu)先繪制骨坑。繪制原理示圖如下:


image.png

這里顯示的三個(gè)item繪制次序,很明顯柬采,正是由于后面的item把前面的item疊加部分蓋住了欢唾,才造成了現(xiàn)在的每個(gè)item只顯示出一半的情況且警。

那如果我們更改下顯示順序,將兩邊的先繪制礁遣,將屏幕中間的Item(當(dāng)前選中的item)最后繪制斑芜,就會(huì)成為這個(gè)情況:


image.png

形成的效果就是本節(jié)開(kāi)篇的效果。

那關(guān)鍵的部分來(lái):要怎么更改Item的繪制順序呢亡脸?

其實(shí)押搪,只需要重寫(xiě)RecyclerView的getChildDrawingOrder方法即可。

該方法的詳細(xì)聲明如下:

protected int getChildDrawingOrder(int childCount, int i)
  • childCount:表示當(dāng)前屏幕上可見(jiàn)的item的個(gè)數(shù)
  • i:表示item的索引浅碾,一般而言大州,i的值就是在list中可見(jiàn)item的順序,通過(guò)getChildAt(i)即可得到當(dāng)前item的視圖垂谢。
  • return int:返回值表示當(dāng)前item的繪制順序厦画,返回值越小,越先繪制滥朱,返回值越大根暑,越最后繪制。很顯然徙邻,要實(shí)現(xiàn)我們開(kāi)篇的效果排嫌,中間item的返回值應(yīng)該是最大的,才能讓它最后繪制缰犁,以顯示在最上面淳地。

需要注意的是,默認(rèn)情況下帅容,即便重寫(xiě)getChildDrawingOrder函數(shù)颇象,代碼也不會(huì)執(zhí)行到getChildDrawingOrder里面的,我們需要在RecyclerView初始化時(shí)并徘,顯式調(diào)用setChildrenDrawingOrderEnabled(true);開(kāi)啟重新排序遣钳。

所以開(kāi)啟重新排序,總共需要有兩步:

  • 1.調(diào)用setChildrenDrawingOrderEnabled(true);開(kāi)啟重新排序
  • 2.在getChildDrawingOrder中重新返回每個(gè)item的繪制順序

2.6.2麦乞、重寫(xiě)RecyclerView

因?yàn)槲覀円貙?xiě)getChildDrawingOrder蕴茴,所以我們必須重寫(xiě)RecylcerView:

public class RecyclerCoverFlowView extends RecyclerView {
    public RecyclerCoverFlowView(Context context) {
        super(context);
        init();
    }

    public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RecyclerCoverFlowView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init(){
        setChildrenDrawingOrderEnabled(true); //開(kāi)啟重新排序
    }

    /**
     * 獲取LayoutManger,并強(qiáng)制轉(zhuǎn)換為CoverFlowLayoutManger
     */
    public CoverFlowLayoutManager getCoverFlowLayout() {
        return ((CoverFlowLayoutManager)getLayoutManager());
    }

    @Override
    protected int getChildDrawingOrder(int childCount, int i) {
       return super.getChildDrawingOrder(childCount, i);
    }
}

在這里路幸,我們主要做了兩步:

在初始化時(shí)荐开,使用setChildrenDrawingOrderEnabled(true);開(kāi)啟重新排序
因?yàn)楹竺妫覀冃枰玫轿覀冏远x的LayoutManager,所以我們額外提供了一個(gè)函數(shù)public CoverFlowLayoutManager getCoverFlowLayout()简肴,以供后續(xù)使用
接下來(lái)晃听,我們就來(lái)看看如何在getChildDrawingOrder中返回對(duì)應(yīng)item的繪制順序。

2.6.3、計(jì)算繪制順序原理

下圖展示了位置索引與繪圖順序的關(guān)系:


image.png

在這個(gè)圖中能扒,總共有7個(gè)Item佣渴,帶有圓圈的0,1初斑,2辛润,3,4,5,6是當(dāng)前在屏幕中顯示的item位置索引,它的值也是默認(rèn)的繪圖順序见秤,默認(rèn)的繪圖順序就是越靠前的item越先繪制砂竖。
要想達(dá)到圖上所示的效果,它的繪圖順序可以是0,1,2,6,5,4,3鹃答;因?yàn)閿?shù)值代表的是繪制順序乎澄,所以值越大的越后繪制,所以左側(cè)的三個(gè)的順序是0测摔,1置济,2;所以锋八,第一個(gè)item先繪制浙于,然后第二個(gè)item蓋在第一個(gè)上面;再然后挟纱,第三個(gè)item再繪制羞酗,它會(huì)蓋在第二個(gè)item的上面。所以這樣就保證的中間卡片左側(cè)部分的疊加效果紊服。右側(cè)三個(gè)的繪制順序是5整慎,4,3围苫;所以最后一個(gè)item先繪制,然后是倒數(shù)第二個(gè)撤师,最后是倒數(shù)第三個(gè)剂府;同樣,右側(cè)三個(gè)也可以保證圖中的疊加效果剃盾。最中間的Item繪制順序?yàn)?腺占,所以最后繪制,所以它會(huì)蓋在所有item的上面顯示出來(lái)痒谴。
注意:我這里講到這個(gè)效果的繪圖順序時(shí)衰伯,說(shuō)的是“可以是”,而不是“必須是”积蔚!意鲸,只要保證下面兩點(diǎn),所有的繪圖順序都是正確的:

繪圖順序的返回值范圍在0到childCount-1之間,其中(childCount表示當(dāng)前屏幕中的可見(jiàn)item個(gè)數(shù))
此繪圖順序在疊加后怎顾,可以保證最終效果
所以读慎,如果我們把繪圖順序改為3,4槐雾,5夭委,6,2募强,1株灸,0;同樣是可以達(dá)到上面的效果的。

為了方便計(jì)算規(guī)則擎值,我們使用0,1,2,6,5,4,3的繪圖順序慌烧。

很明顯,我們需要先找到所有在顯示item的中間位置幅恋,中間位置的繪圖順序是count -1;

然后中間位置之前的繪圖順序和它的排列排序相同杏死,在getChildDrawingOrder函數(shù)中,排列順序是i捆交,那么繪圖順序也是i;

最難的部分是中間位置之后的部分淑翼,它們的繪圖順序怎么算。

很明顯品追,最后一個(gè)item的繪圖順序始終是center(指屏幕顯示的中間item的索引玄括,這里是3)。倒數(shù)第二個(gè)的繪圖順序是center+1,倒數(shù)第三個(gè)的繪圖順序是center+2;從這個(gè)計(jì)算中可以看出肉瓦,后面的item的繪圖順序總是center+m遭京,而m的值就是當(dāng)前的item和最后一個(gè)item所間隔的個(gè)數(shù)。那當(dāng)前item和最后一個(gè)item間隔的個(gè)數(shù)怎么算呢泞莉?它等于count - 1 - i;不知道大家能不能理解哪雕,count-1正常顯示順序下最后一個(gè)item的索引,也就是當(dāng)前可見(jiàn)的item中的最大的索引鲫趁,而i是屏幕中顯示的item的索引斯嚎,也就是上圖圓圈內(nèi)的數(shù)值。所以挨厚,中間后面的item的繪圖順序的計(jì)算方法是center + count - 1- i;

需要非常注意的是堡僻,這里的i是指屏幕中顯示item的索引,總是從0開(kāi)始的疫剃,并不是指在Adapter中所有item中的索引值钉疫。它的意義與getChildAt(i)中的i是一樣的。

所以總結(jié)來(lái)講:

  • 中間位置的繪圖順序?yàn)閛rder = count -1;
  • 中間位置之前的item的繪圖順序?yàn)?order = i;
  • 中間位置之后的item的繪圖順序?yàn)?order = center + count - i - i;

2.6.4巢价、重寫(xiě)getChildDrawingOrder

在理解了如何計(jì)算繪圖順序以后牲阁,現(xiàn)在就開(kāi)始寫(xiě)代碼了固阁,在上面總結(jié)中,可以看到咨油,這里count和 i 都是getChildDrawingOrder中現(xiàn)成的您炉,唯一缺少的就是center值。center值是當(dāng)前可見(jiàn)item中間位置從0開(kāi)始的索引役电。我們可以通過(guò)中間位置的position減去第一個(gè)可見(jiàn)的item的position得到赚爵。

所以,我們需要在CoverFlowLayoutManager中添加一個(gè)函數(shù)(獲取中間item的positon–指在adapter中的position):

public int getCenterPosition(){
    int pos = (int) (mSumDx / getIntervalWidth());
    int more = (int) (mSumDx % getIntervalWidth());
    if (more > getIntervalWidth() * 0.5f) pos++;
    return pos;
}

因?yàn)槲覀兠總€(gè)item的間隔都是getIntervalWidth()法瑟,所以通過(guò)mSumDx / getIntervalWidth()就可以知道當(dāng)前移到了多少個(gè)item了冀膝。因?yàn)槲覀円呀?jīng)將第一個(gè)item移到了中間,所以這里的pos就是移動(dòng)mSumDx以后霎挟,中間位置item的索引窝剖。
但是又因?yàn)槲覀兺ㄟ^(guò)mSumDx / getIntervalWidth()取整數(shù)時(shí),它的結(jié)果是向下取整的酥夭。所以赐纱,但是我們想要在中間item移動(dòng)時(shí),超過(guò)一半就切換到下一個(gè)item顯示熬北。所以我們需要做一個(gè)兼容處理:

int more = (int) (mSumDx % getIntervalWidth());
if (more > getIntervalWidth() * 0.5f) pos++;

利用(int) (mSumDx % getIntervalWidth())得到當(dāng)前正在移動(dòng)的item移動(dòng)過(guò)的距離疙描,如果more大于半個(gè)item的話,那就讓pos++讶隐,將下一個(gè)item標(biāo)記為center起胰,從而讓它最后繪制,顯示在最上層巫延。

在得到中間位置的position之后效五,我們還需要得到第一個(gè)可見(jiàn)的item的position:

public int getFirstVisiblePosition() {
    if (getChildCount() <= 0){
        return 0;
    }

    View view = getChildAt(0);
    int pos = getPosition(view);
    
    return pos;
}

這里的原理也非常簡(jiǎn)單,就是利用getChildAt(0)得到當(dāng)前在顯示的炉峰,第一個(gè)可見(jiàn)的item的View,然后通過(guò)getPosition(View)得到這個(gè)view在Adapter中的position畏妖。

接下來(lái),我們就重寫(xiě)getChildDrawingOrder疼阔,根據(jù)原理可得如下代碼:

protected int getChildDrawingOrder(int childCount, int i) {
    int center = getCoverFlowLayout().getCenterPosition()
            - getCoverFlowLayout().getFirstVisiblePosition(); //計(jì)算正在顯示的所有Item的中間位置
    int order;

    if (i == center) {
        order = childCount - 1;
    } else if (i > center) {
        order = center + childCount - 1 - i;
    } else {
        order = i;
    }
    return order;
}

在獲得繪圖順序的原理理解了之后瓜客,上面的代碼就沒(méi)有難度了,這里就不再細(xì)講了竿开。到這里,我們就實(shí)現(xiàn)了通過(guò)更改繪圖順序的方式玻熙,讓當(dāng)前選中的item在中間全部展示出來(lái)否彩。

這樣,我們修改繪制順序的代碼就完成了嗦随,效果如下圖所示:


繪圖順序.gif

2.7列荔、 添加滾動(dòng)縮放功能

2.7.1敬尺、代碼實(shí)現(xiàn)

在講解《RecyclerView回收實(shí)現(xiàn)方式二》時(shí),我們就已經(jīng)實(shí)現(xiàn)了贴浙,在滾動(dòng)時(shí)讓Item旋轉(zhuǎn)的功能砂吞,其實(shí)非常簡(jiǎn)單,只需要在layoutDecoratedWithMargins后崎溃,調(diào)用setRotate系列函數(shù)即可蜻直,同樣的,我們先寫(xiě)一個(gè)針對(duì)剛添加的ChildView進(jìn)行縮放的函數(shù):

private void handleChildView(View child,int moveX){
    float radio = computeScale(moveX);

    child.setScaleX(radio);
    child.setScaleY(radio);
}

private float computeScale(int x) {
    float scale = 1 -Math.abs(x * 1.0f / (8f*getIntervalWidth()));
    if (scale < 0) scale = 0;
    if (scale > 1) scale = 1;
    return scale;
}

在這兩個(gè)函數(shù)中袁串,handleChildView函數(shù)非常容易理解概而,就是先通過(guò)computeScale(moveX)計(jì)算出一個(gè)要縮放的值,然后調(diào)用setScale系列函數(shù)來(lái)縮放

這里先實(shí)現(xiàn)效果囱修,至于computeScale(moveX)里的公式是如何得來(lái)的赎瑰,我們最后再講解,這里先用著破镰。

接著餐曼,我們需要把handleChildView放在所有的layoutDecoratedWithMargins后,進(jìn)行對(duì)剛布局的view進(jìn)行縮放:

public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {

    …………

    //回收越界子View
    for (int i = getChildCount() - 1; i >= 0; i--) {
        …………
        if (!Rect.intersects(rect, visibleRect)) {
            removeAndRecycleView(child, recycler);
            mHasAttachedItems.put(position, false);
        } else {
            layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
            handleChildView(child,rect.left - mStartX - mSumDx);
            mHasAttachedItems.put(position, true);
        }
    }
    …………
}

private void insertView(int pos, Rect visibleRect, Recycler recycler, boolean firstPos) {
    …………
    if (Rect.intersects(visibleRect, rect) && !mHasAttachedItems.get(pos)) {
        …………
        measureChildWithMargins(child, 0, 0);
        layoutDecoratedWithMargins(child, rect.left - mSumDx, rect.top, rect.right - mSumDx, rect.bottom);
        handleChildView(child,rect.left - mStartX - mSumDx);
        mHasAttachedItems.put(pos, true);
        
    }
}

到這里鲜漩,我們就實(shí)現(xiàn)了開(kāi)篇的效果:


縮放.gif

2.7.2源譬、縮放系數(shù)計(jì)算原理

我們要實(shí)現(xiàn)在卡片滑動(dòng)時(shí)平滑的縮放,所以宇整,在滑動(dòng)過(guò)程中得到的縮放因子肯定是要連續(xù)的瓶佳,所以它的函數(shù)必定是可以用直線或者曲線表示的。

在這里鳞青,我直接用一條直線來(lái)計(jì)算滾動(dòng)過(guò)程中縮放因子霸饲,此直線如下圖所示:


  • Y軸:表示圖片的縮放比例
  • X軸:表示item距離中心點(diǎn)的距離。很明顯臂拓,當(dāng)中間的item的左上角在mStartX上時(shí)厚脉,此時(shí)距離中心點(diǎn)的距離為0,應(yīng)該是最大狀態(tài)胶惰,縮放因子應(yīng)該是1.我這里假設(shè)在相距一個(gè)間距(getIntervalWidth())時(shí)傻工,大小變?yōu)?/8,當(dāng)然這個(gè)值孵滞,大家都可以隨意定中捆。

所以(0,1)、(1坊饶,7/8)這兩個(gè)點(diǎn)就形成了一條直線(兩點(diǎn)連成一條線)泄伪,現(xiàn)在是要利用三角形相似,求出來(lái)這條直線的公式匿级。

image.png

這里根據(jù)三角形相似求出來(lái)公式倒是難度不大蟋滴,但需要注意的是染厅,x軸上的單位是getIntervalWidth(),所以在x軸上1實(shí)際代表的是1*getIntervalWidth()津函;

公式求出來(lái)以后肖粮,就是輸入X值,得到對(duì)應(yīng)的縮放因子尔苦。那值要怎么得到呢涩馆?

我們知道X的意思是當(dāng)前item與startX的間距。當(dāng)間距是0時(shí)蕉堰,得到1。所以x值是:rect.left - mSumDx - mStartX冰寻;

其中rect.left - mSumDx表示的是當(dāng)前item在屏幕上位置乐疆。所以rect.left - mSumDx - mStartX表示的是當(dāng)前item在屏幕上與mStartX的距離。

這樣,縮放系數(shù)的計(jì)算原理就講完了庆寺,當(dāng)然大家也可以使用其它的縮放公式壤圃,而且也并不一定是用直線伍绳,也可以用曲線止毕,無(wú)論用什么公式,但一定要保證是線,不能斷,一旦出現(xiàn)斷裂的情況,就會(huì)導(dǎo)致縮放不順暢,會(huì)出現(xiàn)突然變大或者突然變小的情況。現(xiàn)在利耍,大家就可以根據(jù)自己的知識(shí)儲(chǔ)備自由發(fā)揮了玻佩。

2.8咬崔、bug修復(fù)

這里看似效果效果實(shí)現(xiàn)的非常完美,但是垮斯,當(dāng)你滑動(dòng)到底的時(shí)候,問(wèn)題來(lái)了:


image.png

從圖中可以看到扰肌,在滑動(dòng)到底的時(shí)候,停留在了倒數(shù)第二個(gè)Item被選中的狀態(tài)曙旭,應(yīng)該讓最后一個(gè)item被選中桂躏,才是真正的到底蛮位。那怎么解決呢涣狗?

還記得嗎舶担?我們?cè)谥v解《自定義LayoutManager》中,在剛寫(xiě)好LinearLayoutManager時(shí)凤瘦,到頂和到底后都是可以繼續(xù)上滑和下滑的垂蜗。我們?yōu)榱说巾敽偷降讜r(shí)楷扬,不讓它繼續(xù)滑動(dòng),特地添加了邊界判斷:

public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
    int travel = dx;
    //如果滑動(dòng)到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
        //如果滑動(dòng)到最底部
        travel = mTotalWidth - getHorizontalSpace() - mSumDx;
    }
    …………
}

很明顯贴见,正是到底的時(shí)候烘苹,我們添加了判斷,讓它停留在了最后一個(gè)Item在邊界的狀態(tài)片部。所以镣衡,在這里,我們需要對(duì)到底判斷加以調(diào)整档悠,讓它可滑動(dòng)到最后一個(gè)item被選中的狀態(tài)為止廊鸥。

首先,我們需要求出來(lái)最長(zhǎng)能滾動(dòng)的距離辖所,因?yàn)槊總€(gè)item之間的間距是getIntervalWidth()惰说,當(dāng)一個(gè)item滾動(dòng)距離超過(guò)getIntervalWidth()時(shí),就會(huì)切換到下一個(gè)item被選中缘回,所以一個(gè)item最長(zhǎng)的滾動(dòng)距離其實(shí)是getIntervalWidth()吆视,所以最大的滾動(dòng)距離是:

private int getMaxOffset() {
    return (getItemCount() - 1) * getIntervalWidth();
}

同樣,我們使用在《自定義LayoutManager》中計(jì)算較正travel的方法:

travel + mSumDx = getMaxOffset();
=> travel = getMaxOffset() - mSumDx;

所以酥宴,我們把邊界判斷的代碼改為:

public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
    int travel = dx;
    //如果滑動(dòng)到最頂部
    if (mSumDx + dx < 0) {
        travel = -mSumDx;
    } else  if (mSumDx + dx > getMaxOffset()) {
        //如果滑動(dòng)到最底部
        travel = getMaxOffset()  - mSumDx;
    }
    …………
}

現(xiàn)在修復(fù)了以后啦吧,到底之后就正常了,效果如下圖所示:


image.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末授滓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌般堆,老刑警劉巖在孝,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異淮摔,居然都是意外死亡浑玛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)噩咪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人极阅,你說(shuō)我怎么就攤上這事胃碾。” “怎么了筋搏?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵仆百,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我奔脐,道長(zhǎng)俄周,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任髓迎,我火速辦了婚禮峦朗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘排龄。我一直安慰自己波势,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布橄维。 她就那樣靜靜地躺著尺铣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪争舞。 梳的紋絲不亂的頭發(fā)上凛忿,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音竞川,去河邊找鬼店溢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛流译,可吹牛的內(nèi)容都是我干的逞怨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼福澡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼叠赦!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤除秀,失蹤者是張志新(化名)和其女友劉穎糯累,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體册踩,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泳姐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暂吉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胖秒。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖慕的,靈堂內(nèi)的尸體忽然破棺而出阎肝,到底是詐尸還是另有隱情,我是刑警寧澤肮街,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布风题,位于F島的核電站,受9級(jí)特大地震影響嫉父,放射性物質(zhì)發(fā)生泄漏沛硅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一绕辖、第九天 我趴在偏房一處隱蔽的房頂上張望摇肌。 院中可真熱鬧,春花似錦引镊、人聲如沸朦蕴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)吩抓。三九已至,卻和暖如春赴恨,著一層夾襖步出監(jiān)牢的瞬間疹娶,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工伦连, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雨饺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓惑淳,卻偏偏與公主長(zhǎng)得像额港,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子歧焦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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