最近樓主在忙碌於本身的畢設項目,在畢設當中須要實現一個滑動卡片的效果,樓主花了一點時間本身實現了一下,使用是ItemTouchHelper
和LayoutManager
方式實現的。咱們先來看一下效果: git
ViewPager
就能夠實現了,這是沒錯的,可是
ViewPager
一直有一個詬病--那就是
View
的複用性不高。考慮到性能,
RecyclerView
天然是當之無愧的王者,既然咱們學過
RecyclerView
,爲何不嘗試着實現的呢?
看着這個動畫麻煩,其實咱們將它分爲兩個部分實現就很是簡單了。首先,每一個ItemView
是疊加樣式展示的,這個效果在咱們經常使用到的LayoutManger
沒有這種樣式,因此得須要咱們自定義一個LayoutManager
來實現一個這種樣式。這是其一。 其二,滑動切換的效果怎麼實現呢?還記得咱們以前分析過ItemTouchHelper
這個類嗎?這個類的做用是用來實現側滑刪除以及長按拖動的效果的,而這裏切換卡片的效果就至關於側滑刪除,只不過是側滑時作的動畫不同。這裏的動畫主要包括卡片的位移和角度變化,而ItemTouchHelper
怎麼實現根據手指滑動來作相應的動畫呢?答案就在onChildDraw
方法裏面。 其實,咱們從ItemTouchHelper
的onChildDraw
方法裏面就知道,原生只是作了水平位置的變化,因此,咱們能夠重寫這個方法,從而加上咱們想要的動畫。 這樣來分析,這個動畫是否是很是簡單呢?接下來,咱們從看看代碼吧。github
自定義LayoutManager
的相關知識,我在RecyclerView 源碼分析(七) - 自定義LayoutManager及其相關組件的源碼分析文章裏面已經詳細的解釋了,這裏我就不重複了。咱們直接來看代碼吧,關鍵代碼在於onLayoutChildren
方法裏面:bash
@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設置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能夠側滑
mItemTouchHelper.startSwipe(childViewHolder);
}
return false;
}
});
} else {
// 因爲ItemView會複用,因此必定要設置null
view.setOnTouchListener(null);
}
}
}
複製代碼
相信上面的代碼你們都能看的懂,這裏我就不逐行的解釋了。可是有一點須要咱們特別注意:ide
for (int i = layoutCount - 1; i >= 0; i--) {
// ······
}
複製代碼
這裏咱們是倒着添加View
,也就是一個ItemView
雖然在RecyclerView
的內部index爲0,可是在Adapter
中,倒是layoutCount - 1
,這個在咱們自定義ItemTouchHelper.Callback
時,會有很大的做用。源碼分析
關於ItemTouchHelper
的知識,我在RecyclerView 擴展(二) - 手把手教你認識ItemTouchHelper文章裏面已經詳細的解釋過了,因此在這裏我也不重複了。咱們直接來看實現代碼,關鍵在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;
}
// 跟着角度旋轉
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)));
}
}
}
複製代碼
上面代碼的做用我在註釋已經解釋比較清楚了,這裏就不解釋了。不過這裏還須要一點:動畫
for (int i = 0; i < mMaxVisibleCount - 1; i++) {
// ······
}
複製代碼
這裏我縮放的也是0 ~ mMaxVisibleCount - 1的ItemView
,請記住,這個不是ItemView
在Adapter
中的position
,而是ItemView
在RecyclerView
內部的index值。在前面的LayoutManager
中,我已經解釋過,這倆是反着的。因此這裏應該是0 ~ mMaxVisibleCount - 1。 整個實現就是這麼的簡單,其實還有坑沒有說,好比說:ui
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.setRotation(0f);
}
複製代碼
在clearView
方法裏面必須進行重置,由於ItemView
是複用的,不重置的話會出問題的。 在好比說,必須重寫isItemViewSwipeEnabled
方法(雖然不重寫也沒有問題,可是官方文檔建議重寫):spa
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
複製代碼
使用上面代碼來實現效果以後,咱們會發現一個問題,若是將RecyclerView
放在SwipeRefreshLayout
內部,會出現事件衝突。 我簡單的描述一下事件衝突的狀況:當咱們左右滑動時,這是正常的,每一個ItemView
都是正常的側換;可是一旦上下滑動時,正常來講應該是SwipeRefreshLayout
滑動,可是實際上仍是ItemView
在側滑。 關於解決方案的話,我有兩種方案:1. 重寫SwipeRefreshLayout
的onInterceptTouchEvent
方法,進行事件攔截,讓事件不能傳遞到ItemView
中;2. 取消手動調用ItemTouchHelper
的startSwipe
方法,讓ItemTouchHelper
本身來判斷是否符合側滑的條件。 這裏,我特別的說明一下第一種方法。爲何要特別說明第一種方法呢?由於此方法有很大的問題:1. 會重寫SwipeRefreshLayout
,這個形成了沒必要要的工做,這是其一;2. 重寫了SwipeRefreshLayout
會破壞SwipeRefreshLayout
的結構,這個纔是最大的缺點。 爲何重寫SwipeRefreshLayout
會破壞它的結構呢?咱們能夠從SwipeRefreshLayout
的源碼看出來,SwipeRefreshLayout
不會主動的攔截事件,由於SwipeRefreshLayout
是經過嵌套滑動機制來實現滑動,若是咱們在onInterceptTouchEvent
方法裏面進行事件攔截,就違背了SwipeRefreshLayout
的設計。因此,第一種方法是特別不推薦的!!! 其次,咱們來看看第二種方案的實現方式,第二種方案很是簡單,歸根結底就是兩句話:設計
- 在
Callback
裏面不要重寫isItemViewSwipeEnabled
方法,- 在
LayoutManager
裏面不要在每一個ItemView
的OnTouchListener
裏面調用ItemTouchHelper
的startSwipe
方法。
我在這裏簡單的解釋第二種方式爲何這樣作就不會衝突了,不過要了解爲何不衝突,必須得了解之前爲何會衝突。 SwipeRefreshLayout
自己不會攔截事件,因此全部的事件均可以傳遞到RecyclerView
裏面的每一個ItemView
裏面。由於咱們在OnTouchListener
調用ItemTouchHelper
的startSwipe
表示選中了一個ItemView
能夠側滑,從而致使後面事件都會被該ItemView
消費,進而致使了事件衝突。 而取消startSwipe
方法的調用,讓ItemTouchHelper
本身來選中一個能夠側滑的ItemView
,ItemTouchHelper
自己就處理了上下滑和左右滑的衝突的(若是沒有處理,RecyclerView的上下滑跟ItemView的側滑會衝突)。這就是第二種方式的原理。
爲了方便你們的理解,我將本身的Demo代碼上傳到github,供你們參考:SlideCardDemo