??最近樓主在忙碌于自己的畢設(shè)項目,在畢設(shè)當(dāng)中需要實現(xiàn)一個滑動卡片的效果,樓主花了一點時間自己實現(xiàn)了一下,使用是ItemTouchHelper和LayoutManager方式實現(xiàn)的。我們先來看一下效果:

??上面的效果說難也不難,說不難呢,但是這里面又有很多的小細節(jié)需要注意。
??有人說,這動畫很好做啊,使用
ViewPager就可以實現(xiàn)了,這是沒錯的,但是ViewPager一直有一個詬病--那就是View的復(fù)用性不高??紤]到性能,RecyclerView自然是當(dāng)之無愧的王者,既然我們學(xué)過RecyclerView,為什么不嘗試著實現(xiàn)的呢?
1. 效果分析
??看著這個動畫麻煩,其實我們將它分為兩個部分實現(xiàn)就非常簡單了。首先,每個ItemView是疊加樣式展現(xiàn)的,這個效果在我們常用到的LayoutManger沒有這種樣式,所以得需要我們自定義一個LayoutManager來實現(xiàn)一個這種樣式。這是其一。
??其二,滑動切換的效果怎么實現(xiàn)呢?還記得我們之前分析過ItemTouchHelper這個類嗎?這個類的作用是用來實現(xiàn)側(cè)滑刪除以及長按拖動的效果的,而這里切換卡片的效果就相當(dāng)于側(cè)滑刪除,只不過是側(cè)滑時做的動畫不一樣。這里的動畫主要包括卡片的位移和角度變化,而ItemTouchHelper怎么實現(xiàn)根據(jù)手指滑動來做相應(yīng)的動畫呢?答案就在onChildDraw方法里面。
??其實,我們從ItemTouchHelper的onChildDraw方法里面就知道,原生只是做了水平位置的變化,所以,我們可以重寫這個方法,從而加上我們想要的動畫。
??這樣來分析,這個動畫是不是非常簡單呢?接下來,我們從看看代碼吧。
2. LayoutManager
??自定義LayoutManager的相關(guān)知識,我在RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關(guān)組件的源碼分析文章里面已經(jīng)詳細的解釋了,這里我就不重復(fù)了。我們直接來看代碼吧,關(guān)鍵代碼在于onLayoutChildren方法里面:
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
final int layoutCount = Math.min(getItemCount(), mMaxVisibleCount);
detachAndScrapAttachedViews(recycler);
for (int i = layoutCount - 1; i >= 0; i--) {
final View view = recycler.getViewForPosition(i);
addView(view);
measureChildWithMargins(view, 0, 0);
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,
widthSpace / 2 + getDecoratedMeasuredWidth(view),
heightSpace / 2 + getDecoratedMeasuredHeight(view));
// 給每個ItemView設(shè)置scale
view.setScaleX((float) Math.pow(DEFAULT_SCALE, i));
view.setScaleY((float) Math.pow(DEFAULT_SCALE, i));
if (i == 0) {
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
RecyclerView.ViewHolder childViewHolder = mRecyclerView.getChildViewHolder(v);
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
// 這里需要手動告訴ItemTouchHelper可以側(cè)滑
mItemTouchHelper.startSwipe(childViewHolder);
}
return false;
}
});
} else {
// 由于ItemView會復(fù)用,所以一定要設(shè)置null
view.setOnTouchListener(null);
}
}
}
??相信上面的代碼大家都能看的懂,這里我就不逐行的解釋了。但是有一點需要我們特別注意:
for (int i = layoutCount - 1; i >= 0; i--) {
// ······
}
??這里我們是倒著添加View,也就是一個ItemView雖然在RecyclerView的內(nèi)部index為0,但是在Adapter中,卻是layoutCount - 1,這個在我們自定義ItemTouchHelper.Callback時,會有很大的作用。
3.ItemTouchHelper.Callback
??關(guān)于ItemTouchHelper的知識,我在RecyclerView 擴展(二) - 手把手教你認(rèn)識ItemTouchHelper文章里面已經(jīng)詳細的解釋過了,所以在這里我也不重復(fù)了。我們直接來看實現(xiàn)代碼,關(guān)鍵在onChildDraw方法:
@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
// 跟著手指移動
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
final View itemView = viewHolder.itemView;
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
float ratio = dX / getThreshold(recyclerView, viewHolder);
if (ratio > 1) {
ratio = 1;
} else if (ratio < -1) {
ratio = -1;
}
// 跟著角度旋轉(zhuǎn)
itemView.setRotation(ratio * 15);
for (int i = 0; i < mMaxVisibleCount - 1; i++) {
// 下面的ItemView跟著手指縮放
View child = recyclerView.getChildAt(i);
final float currentScale = (float) Math.pow(DEFAULT_SCALE, 2 - i);
final float nextScale = currentScale / DEFAULT_SCALE;
final float scale = (nextScale - currentScale);
child.setScaleX(Math.min(1, currentScale + scale * Math.abs(ratio)));
child.setScaleY(Math.min(1, currentScale + scale * Math.abs(ratio)));
}
}
}
??上面代碼的作用我在注釋已經(jīng)解釋比較清楚了,這里就不解釋了。不過這里還需要一點:
for (int i = 0; i < mMaxVisibleCount - 1; i++) {
// ······
}
??這里我縮放的也是0 ~ mMaxVisibleCount - 1的ItemView,請記住,這個不是ItemView在Adapter中的position,而是ItemView在RecyclerView內(nèi)部的index值。在前面的LayoutManager中,我已經(jīng)解釋過,這倆是反著的。所以這里應(yīng)該是0 ~ mMaxVisibleCount - 1。
??整個實現(xiàn)就是這么的簡單,其實還有坑沒有說,比如說:
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setRotation(0f);
}
??在clearView方法里面必須進行重置,因為ItemView是復(fù)用的,不重置的話會出問題的。
??在比如說,必須重寫isItemViewSwipeEnabled方法(雖然不重寫也沒有問題,但是官方文檔建議重寫):
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
4. 跟SwipeRefreshLayout事件沖突
??使用上面代碼來實現(xiàn)效果之后,我們會發(fā)現(xiàn)一個問題,如果將RecyclerView放在SwipeRefreshLayout內(nèi)部,會出現(xiàn)事件沖突。
??我簡單的描述一下事件沖突的情況:當(dāng)我們左右滑動時,這是正常的,每個ItemView都是正常的側(cè)換;但是一旦上下滑動時,正常來說應(yīng)該是SwipeRefreshLayout滑動,但是實際上還是ItemView在側(cè)滑。
??關(guān)于解決方案的話,我有兩種方案:1. 重寫SwipeRefreshLayout的onInterceptTouchEvent方法,進行事件攔截,讓事件不能傳遞到ItemView中;2. 取消手動調(diào)用ItemTouchHelper的startSwipe方法,讓ItemTouchHelper自己來判斷是否符合側(cè)滑的條件。
??這里,我特別的說明一下第一種方法。為什么要特別說明第一種方法呢?因為此方法有很大的問題:1. 會重寫SwipeRefreshLayout,這個造成了不必要的工作,這是其一;2. 重寫了SwipeRefreshLayout會破壞SwipeRefreshLayout的結(jié)構(gòu),這個才是最大的缺點。
??為什么重寫SwipeRefreshLayout會破壞它的結(jié)構(gòu)呢?我們可以從SwipeRefreshLayout的源碼看出來,SwipeRefreshLayout不會主動的攔截事件,因為SwipeRefreshLayout是通過嵌套滑動機制來實現(xiàn)滑動,如果我們在onInterceptTouchEvent方法里面進行事件攔截,就違背了SwipeRefreshLayout的設(shè)計。所以,第一種方法是特別不推薦的?。?!
??其次,我們來看看第二種方案的實現(xiàn)方式,第二種方案非常簡單,歸根結(jié)底就是兩句話:
- 在
Callback里面不要重寫isItemViewSwipeEnabled方法,- 在
LayoutManager里面不要在每個ItemView的OnTouchListener里面調(diào)用ItemTouchHelper的startSwipe方法。
??我在這里簡單的解釋第二種方式為什么這樣做就不會沖突了,不過要了解為什么不沖突,必須得了解以前為什么會沖突。
??SwipeRefreshLayout本身不會攔截事件,所以所有的事件都可以傳遞到RecyclerView里面的每個ItemView里面。因為我們在OnTouchListener調(diào)用ItemTouchHelper的startSwipe表示選中了一個ItemView可以側(cè)滑,從而導(dǎo)致后面事件都會被該ItemView消費,進而導(dǎo)致了事件沖突。
??而取消startSwipe方法的調(diào)用,讓ItemTouchHelper自己來選中一個可以側(cè)滑的ItemView,ItemTouchHelper本身就處理了上下滑和左右滑的沖突的(如果沒有處理,RecyclerView的上下滑跟ItemView的側(cè)滑會沖突)。這就是第二種方式的原理。
5. 源碼
??為了方便大家的理解,我將自己的Demo代碼上傳到github,供大家參考:SlideCardDemo