Android 仿QQ側滑刪除—一個滿足ListView、RecyclerView以及其他View通用的側滑刪除
對於側滑刪除已經是見慣不慣的了,我也一直有寫類似QQ那樣的側滑刪除控制元件的想法,雖然研究一段時間的自定義View,然對自定義ViewGroup實戰還是較少,並且側滑刪除還要考慮大量的事件分發機制,比如如何處理子控制元件與父控制元件之間的滑動衝突以及一系列的down->move..move.. ->up操作等等。童哥剛好寫了這麼一個側滑刪除,使用起來不但簡單方便,更重要的是更加優雅的實現瞭解耦,也就是說我們不管是使用ListView還是RecyclerView還是其他ViewGroup中的子View都可以使用此方式實現側滑,具體使用方法是:只需要在XML佈局檔案中用這個自定義側滑刪除類去包裹我們要執行側滑刪除的item即可(比如我們要對ListView實現側滑刪除功能,我們只需要在定義ListView的item佈局檔案時,用我們的自定義類作為item的根佈局即可)
既然已經有輪子了,為啥還要再重複一遍呢?原因很簡單,因為上面已經說過了,首先對ViewGroup實戰偏少,加之對事件分發機制想了解的深入些,剛好童哥的文章中實現的側滑刪除demo中包含了我的知識薄弱點,所以便有了此篇部落格,自己跟著敲一遍確實比只看收穫的多。
扯了這麼多,由於童哥的部落格中介紹的很詳細了,那麼我就把自己在除錯中理解的一些知識以及對事件分發機制大致說一下,因為童哥並沒有主要介紹事件分發這塊
主要解決的問題如下:
1 側滑拉出選單。
2 點選除了這個item的其他位置,選單關閉。並加上了屬性動畫,選單關閉有回彈效果。
3 側滑過程中,不許父控制元件上下滑動(事件攔截)。
4 多指同時滑動,遮蔽後觸控的幾根手指。
5 不會同時展開兩個側滑選單。
6 側滑選單時 攔截了長按事件。
7 側滑時,攔截了點選事件
8 通過開關 isLeftSwipe支援左滑右滑
9 判斷手指起始落點,如果距離屬於滑動了,就遮蔽一切點選事件(和QQ互動一樣)
主要是自定義ViewGroup的知識點,通過重寫onMeasure()方法來告訴父控制元件需要多大尺寸,在onLayout()方法中確定子View(即:childView)的位置。我們需要來遍歷我們的childView
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
//具體的操作邏輯
}
}
下面我們來看下onLayout()方法是如何確定每一個子view的位置的
先把我們的item佈局貼出來,方便理解,注意:引用我們自定義ViewGroup包裹布局時要設定android:clickable=”true”這一屬性
<?xml version="1.0" encoding="utf-8"?>
<com.example.swipedelete.view.SwipeMenuLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
>
<!-- 第一個子view,顯示ListView資料內容-->
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/listview_iv"
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_launcher"/>
<TextView
android:id="@+id/listview_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是listview的item內容"
android:layout_gravity="center_vertical"
android:paddingLeft="5dp"
/>
</LinearLayout>
<!-- 下面是側滑選單項 即:第2+個子view-->
<Button
android:id="@+id/btn_zd"
android:layout_width="50dp"
android:layout_height="match_parent"
android:background="#d9dee4"
android:text="置頂"
android:textColor="@android:color/white"/>
<Button
android:id="@+id/btn_delete"
android:layout_width="50dp"
android:layout_height="match_parent"
android:background="#F76E6B"
android:text="刪除"
android:textColor="@android:color/white"/>
</com.example.swipedelete.view.SwipeMenuLayout>
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//LogUtils.e(TAG, "onLayout() called with: " + "changed = [" + changed + "], l = [" + l + "], t = [" + t + "], r = [" + r + "], b = [" + b + "]");
int childCount = getChildCount();
int left = 0 + getPaddingLeft();
int right = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getVisibility() != GONE) {
if (i == 0) {//第一個子View是內容 寬度設定為全屏
childView.layout(left, getPaddingTop(), left + mMaxWidth, getPaddingTop() + childView.getMeasuredHeight());
left = left + mMaxWidth;
} else {
if (isLeftSwipe) {
childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight());
left = left + childView.getMeasuredWidth();
} else {
childView.layout(right - childView.getMeasuredWidth(), getPaddingTop(), right, getPaddingTop() + childView.getMeasuredHeight());
right = right - childView.getMeasuredWidth();
}
}
}
}
}
這裡需要注意的是,我們側滑刪除根佈局下有三部分(一個用來顯示ListView資料內容的,一個是置頂,一個是刪除)。所以childCount的值為3,因此,需要判斷,當 i = 0時,拿到的是第一個子view,即用來顯示資料內容的,所以設定它的寬為全屏,所以left = getPaddingLeft(),如果沒有設定左內邊距則left = 0;當 i 的值不為0時,分為左側滑選單和右側滑選單,這裡只說左側滑,i 不為0 接著確定第二個子view的位置,由於這三部分是橫向排列的,雖然此時側滑還處於隱藏狀態,但是第二個子view的left肯定是自身的寬 + 第一個子view的寬,從而來確定第二個子view的左邊距位置。第三個childView則依次累加。
側滑時,攔截了點選事件
增加一個變數儲存scaleTouchSlop,這個值是系統定義的,超過這個值即判斷此次動作是在滑動。我們利用這個值判斷是否處於側滑。
我們知道對於ViewGroup中事件分發包括三部分:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent(),也就是常說的:事件分發、事件攔截、事件處理。
下面我們假設一個場景:
當我們點選中間的view內中的某一點時:
1、假設都是返回預設值的情況下,事件分發的順序是從上往下:Activity的dispatchTouchEvent()—>ViewGroup的dispatchTouchEvent()—>View的dispatchTouchEvent()
2、假設都是返回預設值的情況下,事件處理的順序是從下往上:View的onTouchEvent()—>ViewGroup的onTouchEvent()—>Activity的onTouchEvent()
3、dispatchTouchEvent()以及onTouchEvent()有一個共同點就是:當返回值為true時,則消費此次事件,不再傳遞給任何view
4、Activity的dispatchTouchEvent()不管返回true還是false都是消費掉事件,返回super.xxx的時候才會分發給下一級ViewGroup的dispatchTouchEvent(),ViewGroup的dispatchTouchEvent()返回false時,則事件回溯到父類(Activity)的onTouchEvent()處理。
5、ViewGroup的dispatchTouchEvent()返回super.xxx時,事件傳遞給自己的onInterceptTouchEvent()處理,如果onInterceptTouchEvent()返回true,表示攔截,然後交給自己的onTouchEvent()處理(onTouchEvent()返回true則消費掉事件,誰也接收不到此事件,返回false或者super.xxx時,則交給父類的onTouchEvent()處理。),返回false或者super.xxx時會將事件交給下一級View的dispatchTouchEvent()處理。
6、View的dispatchTouchEvent()接收到事件之後,返回值為false,則回溯給父類(ViewGroup)的onTouchEvent()處理,返回值為super.xxx時,則交給自己的onTouchEvent()處理。onTouchEvent()返回true則消費掉事件,誰也接收不到此事件,返回false或者super.xxx時,則交給父類的onTouchEvent()處理。
上面簡單的介紹了down的情況下Activity、ViewGroup、View的事件分發機制。有篇文章通過圖文並茂的介紹了事件分發機制,推薦給大家圖解 Android 事件分發機制
由效果圖可以發現,我們的側滑選單在展開和關閉的時候會有回彈的效果,很炫酷,實現方式是通過屬性動畫實現的,設定了動畫的變化率setInterpolator,展開時設定了:mExpandAnim.setInterpolator(new OvershootInterpolator());關於這幾個屬性動畫變化效果簡單說下:
AccelerateDecelerateInterpolator 在動畫開始與結束的地方速率改變比較慢,在中間的時候加速
AccelerateInterpolator 在動畫開始的地方速率改變比較慢,然後開始加速
AnticipateInterpolator 開始的時候向後然後向前甩
AnticipateOvershootInterpolator 開始的時候向後然後向前甩一定值後返回最後的值
BounceInterpolator 動畫結束的時候彈起
CycleInterpolator 動畫迴圈播放特定的次數,速率改變沿著正弦曲線
DecelerateInterpolator 在動畫開始的地方快然後慢
LinearInterpolator 以常量速率改變
OvershootInterpolator 向前甩一定值後再回到原來位置
如果你不想設定帶回彈效果,你可以不用設定setInterpolator或者你直接設定mExpandAnim.setInterpolator(new LinearInterpolator()); LinearInterpolator():表示勻速