本文已授權微信公衆號:鴻洋(hongyangAndroid)在微信公衆號平臺原創首發。
偶爾看到知乎首頁的側滑刪除,感覺還不錯。之前用RecyclerView的ItemTouchHelper類來實現了Item的拖動和刪除功能,今天帶來的則是純手工打造的一個側滑刪除。老規矩,先看看效果圖:
當滑動的距離小於紅塊的一半,鬆開手指以後,會自動收縮當前item;當滑動的距離超過一半,鬆開手指以後,會自動將當前item刪除。一起看看怎麼實現的吧:
1.準備工作:
(1)數據準備:一個存放數字的List數組來模擬RecyclerView的數據
(2)子Item的佈局:整體線性佈局水平排列,左側是顯示的部分,右側是不顯示的部分,也就是刪除的部分。刪除的部分是一個相對佈局,然後通過滑動的距離來控制字體與圖片的顯示與隱藏。
(3)RecyclerView三要素:RecyclerAdapter,RecyclerViewHolder,LayoutManager依次設置即可。
2.View的滑動實現:
(1)滑動方法:
這裏我是使用View本身提供的scrollTo/scrollBy方法來實現滑動,scrollBy實際上也是調用了scrollTo方法,scrollTo實現的是基於所傳遞參數的絕對滑動,而scrollBy實現的是基於當前位置的相對滑動。
舉個例子:
scrollTo(50,50)會將View位置移動到指定位置,多次調用無效
scrollBy(50,50)會將View位置移動到指定位置,每調用一次會在現有位置基礎上進行移動
結合這個例子分析一下,手指滑動的距離就是整體View移動的距離,那我們可以直接使用scrollBy(x,y)方法來進行處理,將手指滑動的距離作爲第一個參數傳遞進去,而不用考慮當前View滑動的位置。
(2)滑動方向
在Android屏幕直角座標系中,原點在屏幕左上角,向右X爲正,向下Y爲正。
scrollBy()的參數的正負影響滑動的方向,這裏我們只考慮水平方向上的滑動,所以將第二個參數設置爲0。
按我們正常的理解,應該是參數爲負的時候,向座標軸負方向滑動;當參數爲正的時候,向座標軸正方向滑動。
scrollBy()在參數爲負的時候,向座標軸正方向滑動;當參數爲正的時候,向座標軸負方向滑動。
這是因爲在scrollBy()源碼執行過程的最後,會調用這個方法 :
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
其中l,t,r,b爲原來座標點,scrollX,scrollY爲目標座標點,只有當目標座標點值是負數時,負負得正,移動到的位置才爲正數,這樣纔會重新繪製,整體的View就會向座標軸正方向滑動。
綜上,我們想讓子Item從右往左沿X軸的負方向滑動,scrollBy(X,0)中的X一定是大於0的
(3)滑動實現
現在滑動的方法與方向都已經確定了,接下來的重點就是計算滑動的距離,也就是scrollBy(X,0)中的X的大小了。
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
}
break;
case MotionEvent.ACTION_MOVE: {
int scrollX = itemLayout.getScrollX();
int newScrollX = mStartX - x;
if (newScrollX < 0 && scrollX <= 0) {
newScrollX = 0;
} else if (newScrollX > 0 && scrollX >= maxLength) {
newScrollX = 0;
}
if (scrollX > maxLength / 2) {
textView.setVisibility(GONE);
imageView.setVisibility(VISIBLE);
if (isFirst) {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(imageView, "scaleX", 1f, 1.2f, 1f);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(imageView, "scaleY", 1f, 1.2f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(animatorX).with(animatorY);
animSet.setDuration(800);
animSet.start();
isFirst = false;
}
} else {
textView.setVisibility(VISIBLE);
imageView.setVisibility(GONE);
}
itemLayout.scrollBy(newScrollX, 0);
}
break;
case MotionEvent.ACTION_UP: {
}
break;
mStartX = x;
return super.onTouchEvent(event);
}
其中itemLayout爲一個水平的LinearLayout,textView爲LinearLayout中的”刪除”,imageView爲LinearLayout中的眼睛圖片。
移動計算值 = 最開始點座標 - 最後移動到的座標
- 滑動開始的時候,不允許item向右滑動,此時scrollBy(x,0)中的x小於0;滑動的過程中,左右滑動都可以,但getScrollX()小於等於0的時候就不允許繼續滑動。此時將x設置爲0,代表不再滑動
- 滑動距離大於一半的時候,將文字設置爲GONE,圖片設置爲VISIBLE,否則剛好相反。細心的小夥伴會發現,眼睛圖片的顯示有一個從小到大再到小的過程,這裏用的是屬性動畫ObjectAnimator加上組合動畫AnimatorSet實現的,並且進行了一下判斷,讓動畫在滑動過程中只出現一次
- 滑動的距離超過紅塊的距離的時候,不允許item向左滑動,此時scrollBy(x,0)中的x是大於0。此時將x設置爲0,代表不再滑動
3.RecyclerView的滑動實現
前面已經實現了將一個LinearLayout左右進行滑動,現在關鍵就是將這個LinearLayout的滑動與我們RecyclerView的滑動相結合。
解決辦法就是將這個水平排列的LinearLayout作爲子item佈局的一部分,然後再獲取每一個item的LinearLayout就可以進行滑動了。這裏肯定需要一個參數position,只有獲取到item的position才能得到item的LinearLayout,才能進行刪除操作。
(1)通過觸碰的座標計算當前的position
這裏我們肯定要自定義一個MyRecyclerView繼承自RecyclerView,然後重寫onTouchEvent()方法,在MotionEvent.ACTION_DOWN的時候就要拿到你觸碰的item的position。
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//通過點擊的座標計算當前的position
int mFirstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
Rect frame = mTouchFrame;
if (frame == null) {
mTouchFrame = new Rect();
frame = mTouchFrame;
}
final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
child.getHitRect(frame);
if (frame.contains(x, y)) {
pos = mFirstPosition + i;
}
}
}
}
break;
在Listview當中,有一個pointToPosition(x, y)方法可以根據座標獲取到當前的position,在RecyclerView中沒有這個方法,需要我們自己動手寫一個。
這裏有一點特別需要注意的是:這裏遍歷的是當前可見範圍內的子項。使用getChildCount()與getChildAt()進行取值,只能是當前可見區域的子項!取值範圍在0到getLastVisiblePosition()減去getFirstVisiblePosition()之間(可取等於)。
(2)通過position得到item的viewHolder
//通過position得到item的viewHolder
View view = getChildAt(pos - mFirstPosition);
MyViewHolder viewHolder = (MyViewHolder) getChildViewHolder(view);
itemLayout = viewHolder.layout;
textView = (TextView) itemLayout.findViewById(R.id.item_delete_txt);
imageView = (ImageView) itemLayout.findViewById(R.id.item_delete_img);
viewHolder是存放視圖與數據的地方,只要拿到當前item的viewHolder,就可以獲取到我們的itemLayout,也就是需要滑動的LinearLayout。RecyclerView提供了一個getChildViewHolder()的方法來獲取當前item的viewHolder,傳進去的參數就是通過getChildAt(index)獲取到的view。
4.RecyclerView的刪除實現
我們在上一步已經拿到了item的position與itemLayout,在MotionEvent.ACTION_MOVE的時候使用itemLayout就可以進行滑動,在MotionEvent.ACTION_UP的時候使用position就可以進行刪除。
case MotionEvent.ACTION_UP: {
int scrollX = itemLayout.getScrollX();
if (scrollX > maxLength / 2) {
((RecyclerAdapter) getAdapter()).removeRecycle(pos);
}
}
break;
當滑動的距離大於一半的時候,執行刪除操作。 將刪除方法寫在RecyclerAdapter中:
public void removeRecycle(int position) {
lists.remove(position);
notifyDataSetChanged();
if (lists.size() == 0) {
Toast.makeText(context, "已經沒數據啦", Toast.LENGTH_SHORT).show();
}
}
5.RecyclerView的滑動優化
之前說到當滑動的距離小於紅塊的一半,鬆開手指以後,會自動收縮當前item,但是這個滑動比較生硬,用戶體驗很差。我們需要實現漸進式滑動,也就是View的彈性滑動。這裏我們使用的是Scroller。
初始化Scroller:
mScroller = new Scroller(context, new LinearInterpolator(context, null));
第二個參數是一個勻速插值器
Scroller的使用方法:
case MotionEvent.ACTION_UP: {
int scrollX = itemLayout.getScrollX();
if (scrollX > maxLength / 2) {
((RecyclerAdapter) getAdapter()).removeRecycle(pos);
} else {
mScroller.startScroll(scrollX, 0, -scrollX, 0);
invalidate();
}
isFirst = true;
}
break;
startScroll()四個參數依次爲:開始移動時的X座標;開始移動時的Y座標;沿X軸移動距離,爲負時,子控件向右移動;沿Y軸移動距離。如果後面沒有duration這個參數,系統會使用默認的時長:250毫秒
然後調用invalidate()是使view進行重繪,在view的onDraw()方法中又會去調用computeScroll()方法,view才能實現彈性滑動
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
itemLayout.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
首先向Scroller獲取當前的滑動起點,通過scrollTo方法實現滑動,然後再調用invalidate()來進行重繪,又會調用computeScroll()方法,然後再獲取當前的起點,使用scrollTo方法滑動到新的位置。如此往復,直到整個滑動結束。其實Scroller的設計思想就是小幅度滑動組成整個的彈性滑動。
至此,一個漂亮的側滑刪除就已經實現了,零碎的東西不少,記錄下來一起學習~~
補充:
評論裏有小夥伴說加上點擊事件後沒有效果,會產生事件衝突。謝謝這位小夥伴的提醒,之前沒有考慮這方面的問題。然後週末在家完善了一下,看看怎麼解決的吧。
case MotionEvent.ACTION_UP: {
xUp = x;
yUp = y;
int dx = xUp - xDown;
int dy = yUp - yDown;
if (Math.abs(dy) < mTouchSlop && Math.abs(dx) < mTouchSlop) {
listener.getPosition(pos);
} else {
int scrollX = itemLayout.getScrollX();
if (scrollX > maxLength / 2) {
((RecyclerAdapter) getAdapter()).removeRecycle(pos);
} else {
mScroller.startScroll(scrollX, 0, -scrollX, 0);
invalidate();
}
isFirst = true;
}
}
break;
RecyclerView的點擊事件無非就是接口回調獲取position的過程,我們在MotionEvent.ACTION_DOWN的時候已經拿到了position。那麼只要在點擊的時候將這個position傳遞給Activity呢。現在只要判斷什麼動作是點擊就可以了!!!其實只要對比一下MotionEvent.ACTION_DOWN與MotionEvent.ACTION_UP的X,Y座標差,小於默認的滑動最小距離的時候,就認爲是點擊動作,將得到的position傳遞即可。最後讓Activity實現這個接口,獲取參數,進行事件的處理就歐了~