轉(zhuǎn)自RecycerView系列之六實(shí)現(xiàn)滾動(dòng)畫(huà)廊控件
1、滾動(dòng)畫(huà)廊控件
本節(jié)實(shí)現(xiàn)的效果如下圖所示:
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;
}
修改后的效果如下圖所示:
到此已經(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)效果了些侍,效果如下:
完整的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)了卡片疊加的功能辞州,效果如下圖所示:
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;
然后仍稀,在計(jì)算每個(gè)item的rect時(shí)藏姐,將每個(gè)item后移mStartX距離:new Rect(mStartX + offsetX, 0, mStartX + offsetX + mItemWidth, mItemHeight)
就這樣摆出,我們就完成了移動(dòng)初始化位置的功能宅荤,效果如下圖所示:
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)最后繪制斑芜,就會(huì)成為這個(gè)情況:
形成的效果就是本節(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)系:
在這個(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)否彩。
這樣,我們修改繪制順序的代碼就完成了嗦随,效果如下圖所示:
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)篇的效果:
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)這條直線的公式匿级。
這里根據(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)了:
從圖中可以看到扰肌,在滑動(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ù)了以后啦吧,到底之后就正常了,效果如下圖所示: