轉(zhuǎn)自RecycerView系列之六實(shí)現(xiàn)滾動畫廊控件
1、滾動畫廊控件
本節(jié)實(shí)現(xiàn)的效果如下圖所示:

2、實(shí)現(xiàn)橫向布局
2.1、開啟橫向滾動
在自定義LayoutManager之復(fù)用與回收二中已經(jīng)介紹了如何通過自定義LayoutManager實(shí)現(xiàn)垂直滾動的效果,由于本文中效果是中橫向滾動的,故在此基礎(chǔ)上進(jìn)行修改。
首先刪除canScrollVertically()和scrollVerticallyBy函數(shù),改為:
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
…………
}
在經(jīng)過上面修改后,可以成功運(yùn)行,但是布局依然是豎直布局的,很明顯需要修改onLayoutChildren()進(jìn)行橫向布局。
2.2、實(shí)現(xiàn)橫向布局
最關(guān)鍵的問題就是,我們在初始化布局時(shí),會通過mItemRects來保存所有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)是橫向滾動了,已經(jīng)不再是豎向滾動了,所以可見區(qū)域應(yīng)該是橫向滾動后的可見區(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) {//沒有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() / 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的寬度和沒有填滿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;
}
修改后的效果如下圖所示:

到此已經(jīng)實(shí)現(xiàn)了橫向的布局,下面修改下橫向滾動的邏輯。
2.3、實(shí)現(xiàn)橫向滾動
橫向滾動是放在scrollHorizontallyBy中處理,主要的修改如下:
- 1、邊界處理修改
int travel = dx;
//如果滑動到最頂部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
//如果滑動到最底部
travel = mTotalWidth - getHorizontalSpace() - mSumDx;
}
邊界的處理和垂直的處理邏輯大致相同,只需要進(jì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、在移動后需要處理空白區(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)了橫向的滾動效果了,效果如下:

完整的scrollHorizontallyBy代碼如下
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
if (getChildCount() <= 0) {
return dx;
}
int travel = dx;
//如果滑動到最頂部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
//如果滑動到最底部
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è)卡片的大小。所以,我們需要修改排列卡片的代碼,使卡片疊加起來。
首先,申請一個(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í)的起始位置,并沒有更改卡片的大小,所以每個(gè)卡片的長度和寬度是不能變的。
然后在初始化時(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中處理滾動時(shí),每個(gè)卡片的位置都是直接從mItemRects中取的,所以,我們并不需要在修改滾動時(shí)的代碼。
到這里,就實(shí)現(xiàn)了卡片疊加的功能,效果如下圖所示:

2.5、修改卡片的起始位置
到現(xiàn)在,我們卡片都還是在最左側(cè)開始展示的,但在開篇的效果圖中可以看出,在初始化時(shí),第一個(gè)item是在最屏幕中間顯示的,這是怎么做到的呢?
首先,我們需要先申請一個(gè)變量mStartX,來保存卡片后移的距離。
很明顯,這里也只是改變每個(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;

然后,在計(jì)算每個(gè)item的rect時(shí),將每個(gè)item后移mStartX距離:new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight)
就這樣,我們就完成了移動初始化位置的功能,效果如下圖所示:

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)先繪制。繪制原理示圖如下:

這里顯示的三個(gè)item繪制次序,很明顯,正是由于后面的item把前面的item疊加部分蓋住了,才造成了現(xiàn)在的每個(gè)item只顯示出一半的情況。
那如果我們更改下顯示順序,將兩邊的先繪制,將屏幕中間的Item(當(dāng)前選中的item)最后繪制,就會成為這個(gè)情況:

形成的效果就是本節(jié)開篇的效果。
那關(guān)鍵的部分來:要怎么更改Item的繪制順序呢?
其實(shí),只需要重寫RecyclerView的getChildDrawingOrder方法即可。
該方法的詳細(xì)聲明如下:
protected int getChildDrawingOrder(int childCount, int i)
- childCount:表示當(dāng)前屏幕上可見的item的個(gè)數(shù)
- i:表示item的索引,一般而言,i的值就是在list中可見item的順序,通過getChildAt(i)即可得到當(dāng)前item的視圖。
- return int:返回值表示當(dāng)前item的繪制順序,返回值越小,越先繪制,返回值越大,越最后繪制。很顯然,要實(shí)現(xiàn)我們開篇的效果,中間item的返回值應(yīng)該是最大的,才能讓它最后繪制,以顯示在最上面。
需要注意的是,默認(rèn)情況下,即便重寫getChildDrawingOrder函數(shù),代碼也不會執(zhí)行到getChildDrawingOrder里面的,我們需要在RecyclerView初始化時(shí),顯式調(diào)用setChildrenDrawingOrderEnabled(true);開啟重新排序。
所以開啟重新排序,總共需要有兩步:
- 1.調(diào)用setChildrenDrawingOrderEnabled(true);開啟重新排序
- 2.在getChildDrawingOrder中重新返回每個(gè)item的繪制順序
2.6.2、重寫RecyclerView
因?yàn)槲覀円貙慻etChildDrawingOrder,所以我們必須重寫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); //開啟重新排序
}
/**
* 獲取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);開啟重新排序
因?yàn)楹竺?,我們需要用到我們自定義的LayoutManager,所以我們額外提供了一個(gè)函數(shù)public CoverFlowLayoutManager getCoverFlowLayout(),以供后續(xù)使用
接下來,我們就來看看如何在getChildDrawingOrder中返回對應(yīng)item的繪制順序。
2.6.3、計(jì)算繪制順序原理
下圖展示了位置索引與繪圖順序的關(guān)系:

在這個(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再繪制,它會蓋在第二個(gè)item的上面。所以這樣就保證的中間卡片左側(cè)部分的疊加效果。右側(cè)三個(gè)的繪制順序是5,4,3;所以最后一個(gè)item先繪制,然后是倒數(shù)第二個(gè),最后是倒數(shù)第三個(gè);同樣,右側(cè)三個(gè)也可以保證圖中的疊加效果。最中間的Item繪制順序?yàn)?,所以最后繪制,所以它會蓋在所有item的上面顯示出來。
注意:我這里講到這個(gè)效果的繪圖順序時(shí),說的是“可以是”,而不是“必須是”!,只要保證下面兩點(diǎn),所有的繪圖順序都是正確的:
繪圖順序的返回值范圍在0到childCount-1之間,其中(childCount表示當(dāng)前屏幕中的可見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)前可見的item中的最大的索引,而i是屏幕中顯示的item的索引,也就是上圖圓圈內(nèi)的數(shù)值。所以,中間后面的item的繪圖順序的計(jì)算方法是center + count - 1- i;
需要非常注意的是,這里的i是指屏幕中顯示item的索引,總是從0開始的,并不是指在Adapter中所有item中的索引值。它的意義與getChildAt(i)中的i是一樣的。
所以總結(jié)來講:
- 中間位置的繪圖順序?yàn)閛rder = count -1;
- 中間位置之前的item的繪圖順序?yàn)?order = i;
- 中間位置之后的item的繪圖順序?yàn)?order = center + count - i - i;
2.6.4、重寫getChildDrawingOrder
在理解了如何計(jì)算繪圖順序以后,現(xiàn)在就開始寫代碼了,在上面總結(jié)中,可以看到,這里count和 i 都是getChildDrawingOrder中現(xiàn)成的,唯一缺少的就是center值。center值是當(dāng)前可見item中間位置從0開始的索引。我們可以通過中間位置的position減去第一個(gè)可見的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(),所以通過mSumDx / getIntervalWidth()就可以知道當(dāng)前移到了多少個(gè)item了。因?yàn)槲覀円呀?jīng)將第一個(gè)item移到了中間,所以這里的pos就是移動mSumDx以后,中間位置item的索引。
但是又因?yàn)槲覀兺ㄟ^mSumDx / getIntervalWidth()取整數(shù)時(shí),它的結(jié)果是向下取整的。所以,但是我們想要在中間item移動時(shí),超過一半就切換到下一個(gè)item顯示。所以我們需要做一個(gè)兼容處理:
int more = (int) (mSumDx % getIntervalWidth());
if (more > getIntervalWidth() * 0.5f) pos++;
利用(int) (mSumDx % getIntervalWidth())得到當(dāng)前正在移動的item移動過的距離,如果more大于半個(gè)item的話,那就讓pos++,將下一個(gè)item標(biāo)記為center,從而讓它最后繪制,顯示在最上層。
在得到中間位置的position之后,我們還需要得到第一個(gè)可見的item的position:
public int getFirstVisiblePosition() {
if (getChildCount() <= 0){
return 0;
}
View view = getChildAt(0);
int pos = getPosition(view);
return pos;
}
這里的原理也非常簡單,就是利用getChildAt(0)得到當(dāng)前在顯示的,第一個(gè)可見的item的View,然后通過getPosition(View)得到這個(gè)view在Adapter中的position。
接下來,我們就重寫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;
}
在獲得繪圖順序的原理理解了之后,上面的代碼就沒有難度了,這里就不再細(xì)講了。到這里,我們就實(shí)現(xiàn)了通過更改繪圖順序的方式,讓當(dāng)前選中的item在中間全部展示出來。
這樣,我們修改繪制順序的代碼就完成了,效果如下圖所示:

2.7、 添加滾動縮放功能
2.7.1、代碼實(shí)現(xiàn)
在講解《RecyclerView回收實(shí)現(xiàn)方式二》時(shí),我們就已經(jīng)實(shí)現(xiàn)了,在滾動時(shí)讓Item旋轉(zhuǎn)的功能,其實(shí)非常簡單,只需要在layoutDecoratedWithMargins后,調(diào)用setRotate系列函數(shù)即可,同樣的,我們先寫一個(gè)針對剛添加的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ù)非常容易理解,就是先通過computeScale(moveX)計(jì)算出一個(gè)要縮放的值,然后調(diào)用setScale系列函數(shù)來縮放
這里先實(shí)現(xiàn)效果,至于computeScale(moveX)里的公式是如何得來的,我們最后再講解,這里先用著。
接著,我們需要把handleChildView放在所有的layoutDecoratedWithMargins后,進(jìn)行對剛布局的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)了開篇的效果:

2.7.2、縮放系數(shù)計(jì)算原理
我們要實(shí)現(xiàn)在卡片滑動時(shí)平滑的縮放,所以,在滑動過程中得到的縮放因子肯定是要連續(xù)的,所以它的函數(shù)必定是可以用直線或者曲線表示的。
在這里,我直接用一條直線來計(jì)算滾動過程中縮放因子,此直線如下圖所示:

- 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)在是要利用三角形相似,求出來這條直線的公式。

這里根據(jù)三角形相似求出來公式倒是難度不大,但需要注意的是,x軸上的單位是getIntervalWidth(),所以在x軸上1實(shí)際代表的是1*getIntervalWidth();
公式求出來以后,就是輸入X值,得到對應(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)然大家也可以使用其它的縮放公式,而且也并不一定是用直線,也可以用曲線,無論用什么公式,但一定要保證是線,不能斷,一旦出現(xiàn)斷裂的情況,就會導(dǎo)致縮放不順暢,會出現(xiàn)突然變大或者突然變小的情況?,F(xiàn)在,大家就可以根據(jù)自己的知識儲備自由發(fā)揮了。
2.8、bug修復(fù)
這里看似效果效果實(shí)現(xiàn)的非常完美,但是,當(dāng)你滑動到底的時(shí)候,問題來了:

從圖中可以看到,在滑動到底的時(shí)候,停留在了倒數(shù)第二個(gè)Item被選中的狀態(tài),應(yīng)該讓最后一個(gè)item被選中,才是真正的到底。那怎么解決呢?
還記得嗎?我們在講解《自定義LayoutManager》中,在剛寫好LinearLayoutManager時(shí),到頂和到底后都是可以繼續(xù)上滑和下滑的。我們?yōu)榱说巾敽偷降讜r(shí),不讓它繼續(xù)滑動,特地添加了邊界判斷:
public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
int travel = dx;
//如果滑動到最頂部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > mTotalWidth - getHorizontalSpace()) {
//如果滑動到最底部
travel = mTotalWidth - getHorizontalSpace() - mSumDx;
}
…………
}
很明顯,正是到底的時(shí)候,我們添加了判斷,讓它停留在了最后一個(gè)Item在邊界的狀態(tài)。所以,在這里,我們需要對到底判斷加以調(diào)整,讓它可滑動到最后一個(gè)item被選中的狀態(tài)為止。
首先,我們需要求出來最長能滾動的距離,因?yàn)槊總€(gè)item之間的間距是getIntervalWidth(),當(dāng)一個(gè)item滾動距離超過getIntervalWidth()時(shí),就會切換到下一個(gè)item被選中,所以一個(gè)item最長的滾動距離其實(shí)是getIntervalWidth(),所以最大的滾動距離是:
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;
//如果滑動到最頂部
if (mSumDx + dx < 0) {
travel = -mSumDx;
} else if (mSumDx + dx > getMaxOffset()) {
//如果滑動到最底部
travel = getMaxOffset() - mSumDx;
}
…………
}
現(xiàn)在修復(fù)了以后,到底之后就正常了,效果如下圖所示:
