前言
之前在Github上看見了一個卡片式滑動的效果,非常的炫酷,當時就想著怎么去實現,剛開始我的構思是自定義一個ViewGroup,但通過自定義ViewGroup實現起來會非常復雜,要對子View位置進行擺放、重寫onTouchEvent結合Scroller進行拖拽、對子View進行動畫設置、最后還要對子View進行復用,然后我就想有沒有其他的實現方式,首先考慮到了RecyclerView,因為RecyclerView.LayoutManager可以對子View位置進行設置,而且可以通過ItemTouchHelper拖拽子View,同時RecyclerView具備復用功能,所以我就嘗試著通過RecyclerView來實現卡片式滑動,最終效果還不錯,特此開一篇文章與大家分享實現流程。
在真正描述 自定義LayoutManager實現前我先把效果圖亮出來讓大家爽一波
閱讀本篇文章的你需要具備的技能:RecyclerView基本使用、LayoutManager、ItemTouchHelper、RecyclerView復用機制、View基本動畫,如果對這些知識點還不熟悉推薦閱讀啟艦大神的RecyclerView系列
1.自定義LayoutManager
首先定義一個類CardManager繼承RecyclerView.LayoutManager,重寫generateDefaultLayoutParams()方法和onLayoutChildren()方法,代碼如下
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
detachAndScrapAttachedViews(recycler);
int itemCount = getItemCount();
//如果RecyclerView中只有一個item或者沒有,什么都不做
if(itemCount<1){
return;
}
//最底部item的角標
int bottomPosition;
if(itemCount<MAX_COUNT){
bottomPosition = 0;
}else {
bottomPosition = itemCount - MAX_COUNT;
}
//從最底層的item開始擺放
for(int i =bottomPosition;i<itemCount;i++){
//從緩沖池中獲取到itemView
View view = recycler.getViewForPosition(i);
//將itemView添加到RecyclerView中
addView(view);
//測量itemView
measureChildWithMargins(view,0,0);
//recyclerView寬度-itemView寬度
int widthSpace = getWidth() - getDecoratedMeasuredWidth(view);
int heightSpace = getHeight() - getDecoratedMeasuredHeight(view);
//將itemView水平居中
layoutDecoratedWithMargins(view,widthSpace/2,heightSpace/2
,widthSpace/2+getDecoratedMeasuredWidth(view)
,heightSpace/2+getDecoratedMeasuredHeight(view));
//改變View的大小跟位置
int level = Math.abs(i-itemCount+1);
if(level>MAX_COUNT-3){
view.setScaleX(1-SCALE_RATIO*(MAX_COUNT-2));
view.setTranslationY(TRANS_RATIO*(MAX_COUNT-2));
}else if(level>0){
view.setScaleX(1-SCALE_RATIO*level);
view.setTranslationY(TRANS_RATIO*level);
}
}
}
generateDefaultLayoutParams()方法的作用是來設置子Item的LayoutParams,此處我們直接設置為自適應wrap_content。
onLayoutChildren()為自定義item位置的核心方法,首先執行detachAndScrapAttachedViews(recycler)方法,將所有的holderView從RecyclerView中剝離出來,等待重新布局。MAX_COUNT為最大View個數,這個取決于用戶個人需求,本篇文章中MAX_COUNT為4,通過MAX_COUNT計算出最底部item的角標。然后通過for循環對item進行布局,for循環中內容我來分步為大家解析:
- 調用getViewForPosition(i)從RecyclerView的緩沖池中獲取到itemView并通過addView()加入到RecyclerView中
- 調用measureChildWithMargins()測量itemView寬高,如不進行測量獲取到的寬高為0
- 調用layoutDecoratedWithMargins()將itemView居中,參數分別為itemView、LEFT、TOP、RIGHT、BOTTOM四種邊距
- 對itemView進行平移和縮放操作,注意:最底層的兩個View大小和位置全部一致,因為在進行拖動的時候倒數第二個itemView會逐漸平移和縮放,所以要多添加一個看不見的itemView讓底部動畫看起來更加的和諧。
LayoutManager我么定義好了,將它設置到RecyclerView的setLayoutManager(manager)中,適配器和常規的定義方法相同,我們來看一下效果圖:
一不小心就實現了,是不是很簡單?但還不夠,這樣只能看到我
大威少
一人,是不能拖拽滑動的,怎么實現拖拽?聰明的同學可能已經想到,通過ItemTouchHelper,沒錯,就是它,我們接著往下看。
2.定義ItemTouchHelper
創建一個類,繼承自ItemTouchHelper.Callback,實現其抽象方法,并重寫onChildDraw()方法,代碼如下:
//定義滑動方向
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = 0;
int swipeFlags = 0;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof CardManager) {
//允許上下滑動
swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT |
ItemTouchHelper.UP | ItemTouchHelper.DOWN ;
}
return makeMovementFlags(dragFlags, swipeFlags);
}
//itemView滑出了屏幕
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
Log.i("zs","direction "+direction);
if(mListener!=null){
mListener.onSwiped(viewHolder.getAdapterPosition(),direction);
}
}
//拖動itemView時對部分itemView施加動畫效果
@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);
float trans;
//以偏移量大的方向為標準
if(Math.abs(dX)>Math.abs(dY)){
trans = Math.abs(dX);
}else {
trans = Math.abs(dY);
}
//滑動比例
float ratio = trans/getThreshold(recyclerView,viewHolder);
if(ratio>1){
ratio = 1;
}
//獲取itemView總量
int itemCount = recyclerView.getChildCount();
//移除時為底部顯示的View增加動畫
for (int i = 1;i<CardManager.MAX_COUNT-1;i++){
View view = recyclerView.getChildAt(i);
float t = 1/(1-CardManager.SCALE_RATIO*ratio)-CardManager.SCALE_RATIO*(itemCount-i-1);
view.setScaleX(t);
view.setTranslationY(-CardManager.TRANS_RATIO*ratio+CardManager.TRANS_RATIO*(itemCount-i-1));
}
//為被拖動的View增加透明度動畫
View view = recyclerView.getChildAt(itemCount-1);
view.setAlpha(1 - Math.abs(ratio) * 0.2f);
}
//獲取劃出屏幕的距離閾值
private float getThreshold(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return recyclerView.getWidth() * getSwipeThreshold(viewHolder);
}
- getMovementFlags():用來設置滑動和拖動方向,兩個參數為滑動和拖動方向,本例中只支持滑動不支持拖動,滑動支持上下左右四個方向。
- onSwiped():當itemView滑出屏幕后會回調該方法,我定義了一個回調接口,在onSwiped()中調用該接口通知適配器做更新操作。
- onChildDraw():滑動時動畫可以在該方法內進行。
- getThreshold():用來獲取劃出屏幕距離閾值,比如通過setThreshold()設置了滑動閾值為100像素,當itemView滑動超過100像素時松手itemView會自定滑出屏幕。
以上幾個方法只有onChildDraw()略微麻煩,其他幾個都比較簡單,所以我只對onChildDraw()中內容進行消息描述:
- 從方法中獲取到x、y軸平移的距離dx、dy,然后從二者中選出一個絕對值較大的作為滑動距離。
- 計算滑動比例ratio,用來設置動畫
- 獲取到總item數,開啟一個for循環逐個為itemView設置平緩度過的動畫 (除去第一個和最后一個),進行平移和縮放的時候一定要建立在之前的基礎上
- 獲取到正在被滑動的itemVIew,為其施加透明度動畫
在實現平移和縮放動畫的時候一定要在已經進行的平移和縮放的基礎上進行,一定一定一定,重要的事情說三遍。以上為過度動畫的內容,難度也不是很高。
Activity中的應用
與RecyclerView進行綁定
CardHelperCallback itemTouchHelpCallback = new CardHelperCallback();
ItemTouchHelper helper = new ItemTouchHelper(itemTouchHelpCallback);
//將ItemTouchHelper和RecyclerView進行綁定
helper.attachToRecyclerView(recyclerView);
一般要在自定義ItemTouchHelper.Callback中定義一個接口,用來itemView被滑出屏幕后即onSwiped()方法被調用的與Adapter通信
public interface OnItemTouchCallbackListener {
//item被滑動的回調方法
void onSwiped(int position, int direction);
}
在需要的地方(一般都是在Activity中)實現接口中的方法onSwiped(int position, int direction)
//實現OnItemTouchCallbackListener 接口
@Override
public void onSwiped(int position,int direction) {
if(direction==CardManager.MAX_COUNT){//左滑
//Toast.makeText(this,"left",Toast.LENGTH_SHORT).show();
}else {//右滑
//Toast.makeText(this,"right",Toast.LENGTH_SHORT).show();
}
if(mCardBeanList!=null){
for (int i = 0;i<recyclerView.getChildCount();i++){
View view = recyclerView.getChildAt(i);
view.setAlpha(1);
}
mCardBeanList.remove(position);
//加載更多
if(mCardBeanList.size()<CardManager.MAX_COUNT){
loadMore();
}
mCardAdapter.notifyDataSetChanged();
}
}
改方法中有三點需要注意:
- 重置itemView的透明度,因為itemView是要進行復用的,所以放入緩沖池中時要進行重置
- 刪除List中對用的元素
- 更新適配器,讓數據重新填充
拓展
我們都知道RecyclerView是可以對View進行復用的,那么它的復用原理是什么呢?在這我就簡單說一下,雖然View緩沖池由RecyclerView掌控,但是想要完成整個View的復用需要LayoutManager配合,因為RecyclerView并不知道什么時候需要將View回收,所以需要LayoutManager告訴RecyclerView回收哪個View,通過recyclerView.removeAndRecycleView(child, recycler)來實現,所以在本例中應該在onSwiped()中調用該方法,但細心的同學可能發現我并未調用這個方法通知RecyclerView回收,其實是因為itemView被滑出屏幕后ItemTouchHelper內部會對該itemView進行回收操作,關于這一塊內容大家做一個了解就行了,如果想要深入了解RecyclerView復用機制可以參考文章開頭推薦的啟艦大神RecyclerView系列。
Demo已托管至github,可運行
總結
RecyclerView是Android中非常重要的一個控件,功能十分強大,并且Google對它的封裝只能用完美的不能再完美
來形容,Adapter、ItemTouchHepler、LayoutManager職責明確,完全獨立于RecyclerView存在,嚴格遵守低耦合
,所以在我們制作卡片式滑動時RecyclerView和Adapter未受到任何影響,這也使得開發者更加清晰的去使用RecyclerView。看了本篇文章相信你對ItemTouchHepler和LayoutManager的理解又更加的深入了一些,如果覺得我幫助到你了,就去github上給我一個start,再次萬分感謝。