1. 程式人生 > >一步整合側滑(刪除)選單,高仿QQ、IOS條目側滑刪除

一步整合側滑(刪除)選單,高仿QQ、IOS條目側滑刪除

本控制元件不依賴任何父佈局,不是針對 RecyclerView、ListView,而是任意的ViewGroup裡的childView都可以使用側滑(刪除)選單。 
簡單,0耦合,支援任意ViewGroup。

概述

本控制元件從擼出來在專案使用至今已經過去7個月,距離第一次將它push至github上,也已經2月+。(之前,我發表過一篇文章。傳送門:http://blog.csdn.net/zxt0601/article/details/52303781, 裡面詳細描述了本控制元件V1.0版本是怎麼實現的。)

期間有很多朋友在評論、issue裡提出了一些改進意見,例如支援設定滑動方向(左右)、高仿QQ的互動、支援GridLayoutManager等,以及一些bug

。已經被我全部實、修復。並且將其打包至jitpack,引入更方便。和第一版相比,改動挺多的。故將其整理,新發一版。

那麼本文先從如何使用它講起,然後介紹它包含的特性、支援的屬性。最後就幾個難點和衝突的解決進行講解。

先上幾個gif給各位看官感受一下最新版的魅力(以下版本都順便展示了可選的雙向滑動)

本控制元件最大魅力就是0耦合,所以先上配合我另一個庫組裝的效果(ItemDecorationIndexBar + SwipeMenuLayout): 
(ItemDecorationIndexBar : https://github.com/mcxtzhang/ItemDecorationIndexBar

)

Android Special Version (無阻塞式,側滑選單展開時,依然可以展開其他側滑選單,同時上一個選單會自動關閉):

GridLayoutManager (和上圖的程式碼比,只需修改RecyclerView的LayoutManager。):

LinearLayout (不需任何修改,連LinearLayout也可以簡單的實現側滑選單):

iOS interaction (阻塞式互動,高仿QQ,側滑選單展開式,遮蔽其他ITEM所有操作):

使用:

Step 1. 在專案根build.gradle檔案中增加JitPack倉庫依賴。

    allprojects {
        repositories {
            ...
maven { url "https://jitpack.io" } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Step 2. Add the dependency

    dependencies {
             compile 'com.github.mcxtzhang:SwipeDelMenuLayout:V1.2.1'
    }
  • 1
  • 2
  • 3

Step 3. 在需要側滑刪除的ContentItem外面套上本控制元件,在本控制元件內依次排列ContentItem、選單即可: 
至此 您就可以使用高仿IOS、QQ 側滑刪除選單功能了 
(側滑選單的點選事件等是通過設定的id取到,與其他控制元件一致,不再贅述)

Demo裡,我的ContentItem是一個TextView,那麼我就在其外巢狀本控制元件,並且以側滑選單出現的順序,依次排列選單控制元件即可。

<?xml version="1.0" encoding="utf-8"?>
<com.mcxtzhang.swipemenulib.SwipeMenuLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:clickable="true"
    android:paddingBottom="1dp">

    <TextView
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="?android:attr/selectableItemBackground"
        android:gravity="center"
        android:text="專案中我是任意複雜的原ContentItem佈局"/>

    <!-- 以下都是側滑選單的內容依序排列 -->
    <Button
        android:id="@+id/btnTop"
        android:layout_width="60dp"
        android:layout_height="match_parent"
        android:background="#d9dee4"
        android:text="置頂"
        android:textColor="@android:color/white"/>

    <Button
        android:id="@+id/btnUnRead"
        android:layout_width="120dp"
        android:layout_height="match_parent"
        android:background="#ecd50a"
        android:clickable="true"
        android:text="標記未讀"
        android:textColor="@android:color/white"/>

    <Button
        android:id="@+id/btnDelete"
        android:layout_width="60dp"
        android:layout_height="match_parent"
        android:background="@color/red_ff4a57"
        android:text="刪除"
        android:textColor="@android:color/white"/>

</com.mcxtzhang.swipemenulib.SwipeMenuLayout>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

支援屬性:

1 通過 isIos 變數控制是否是IOS阻塞式互動,預設是開啟的。 
2 通過 isSwipeEnable 變數控制是否開啟右滑選單,預設開啟。(某些場景,複用item,沒有編輯許可權的使用者不能右滑) 
3 通過開關 isLeftSwipe支援左滑右滑

有兩種方式設定: 
一:xml:

<com.mcxtzhang.swipemenulib.SwipeMenuLayout
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:ios="false"
    app:leftSwipe="true"
    app:swipeEnable="true">
  • 1
  • 2
  • 3
  • 4
  • 5

二: java程式碼:

//這句話關掉IOS阻塞式互動效果 並依次開啟左滑右滑  禁用掉側滑選單
((SwipeMenuLayout) holder.itemView).setIos(false).setLeftSwipe(position % 2 == 0 ? true : false).setSwipeEnable(false);
  • 1
  • 2

支援特性:

  • 不會同時展開2+個側滑選單。(可見介面上最多隻會出現一個側滑選單)。
  • 側滑過程中,禁止父控制元件上下滑動。
  • 多指同時滑動,遮蔽後觸控的幾根手指。
  • 增加viewChache 的 get()方法,可以用在:當點選外部空白處時,關閉正在展開的側滑選單。
  • 以第一個子Item(即ContentItem)的寬度為控制元件寬度

每次更新的checklist:

由於持續迭代,會發生完成一個feature、fix一個bug後,導致新的bug。 
so,整理一份checkList,供每次迭代後驗證,都通過,才會push到github庫上。

專案 備註 驗證
isIos 切換至IOS阻塞互動模式、Android特色無阻塞互動模式 以下feature都可正常工作
isSwipeEnable 是否支援關閉側滑功能
isLeftSwipe 是否支援雙向滑動
ContentItem內容可單擊
ContentItem內容可長按
側滑選單顯示時,ContentItem不可點選
側滑選單顯示時,ContentItem不可長按
側滑選單顯示時,側滑選單可以點選
側滑選單顯示時,點選ContentItem區域關閉選單
側滑過程中,遮蔽長按事件
通過滑動關閉選單,不應該觸發ContentItem點選事件

難點、衝突的解決:

1 ContentItem的長按和本控制元件側滑的衝突。 
一開始我還是老思路,一直都是通過判斷手指起始落點的座標,判斷手指落點處於何位置,遮蔽一些操作。當本控制元件功能越來越龐大,計算開始複雜,也容易出錯。但也跌跌撞撞的撐了過來,但就在今天,我想到了另一種思路:通過禁用子View的longClickable屬性來完成這個功能。於是我重構這部分程式碼,先利用git 檢視之前的提交,去除掉那部分程式碼,並 
在側滑選單展開smoothExpand()、關閉smoothClose()的函式中分別加入:

        //2016 11 13 add 側滑選單展開,遮蔽content長按
        if (null != mContentView) {
            mContentView.setLongClickable(true);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
        //2016 11 13 add 側滑選單展開,遮蔽content長按
        if (null != mContentView) {
            mContentView.setLongClickable(true);
        }
  • 1
  • 2
  • 3
  • 4

程式碼就這麼簡單,幾小時前我才想到這麼簡單的解決方法,這也是促使我新擼一遍文章,記錄本控制元件的一些改動的緣由之一。

2 如何支援任意父控制元件 
這算是本控制元件最酷炫的魅力之一吧,在之前的文章 ,我詳細描述了實現過程。 
總結起來,我利用了一個static變數,儲存了當前正在展開的View, 
在進行各項操作時就可預先判斷,如若會引起衝突,例如介面上出現兩個側滑選單, 
便自動關閉上一個選單。

程式碼如下:

    //儲存的是當前正在展開的View
    private static SwipeMenuLayout mViewCache;
  • 1
  • 2
  • 3

dispatchTouchEvent()的ActionDown裡:

                    //如果down,view和cacheview不一樣,則立馬讓它還原。且把它置為null
                    if (mViewCache != null) {
                        if (mViewCache != this) {
                            mViewCache.smoothClose();
                        }
                        //只要有一個側滑選單處於開啟狀態, 就不給外層佈局上下滑動了
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在側滑選單展開smoothExpand()、關閉smoothClose()的函式:

    //展開就加入ViewCache:
    mViewCache = SwipeMenuLayout.this;
  • 1
  • 2
    //關閉置為null
    mViewCache = null;
  • 1
  • 2

onDetachedFromWindow()裡:

    //每次ViewDetach的時候,判斷一下 ViewCache是不是自己,如果是自己,關閉側滑選單,且ViewCache設定為null,
    // 理由:1 防止記憶體洩漏(ViewCache是一個靜態變數)
    // 2 側滑刪除後自己後,這個View被Recycler回收,複用,下一個進入螢幕的View的狀態應該是普通狀態,而不是展開狀態。
    @Override
    protected void onDetachedFromWindow() {
        if (this == mViewCache) {
            mViewCache.smoothClose();
            mViewCache = null;
        }
        super.onDetachedFromWindow();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3 解決多指滑動衝突: 
利用一個布林值 flag,在ActionDown時判斷是否繼續接受觸控事件: 
程式碼如下:

    //防止多隻手指一起滑我的flag 在每次down裡判斷, touch事件結束清空
    private static boolean isTouching;
  • 1
  • 2

dispatchTouchEvent()的ActionDown裡:

    if (isTouching) {//如果有別的指頭摸過了,那麼就return false。這樣後續的move..等事件也不會再來找這個View了。
        return false;
    } else {
        isTouching = true;//第一個摸的指頭,趕緊改變標誌,宣誓主權。
    }
  • 1
  • 2
  • 3
  • 4
  • 5

ActionUp裡:

    isTouching = false;//沒有手指在摸我了
  • 1

4 支援GridLayoutManager 
畢竟專案中在網格佈局中使用側滑選單還屬少數,所以一開始我將場景簡單化,給本控制元件設定的寬度都是父控制元件的寬度-padding。後來有童鞋提出希望支援網格佈局,一開始我思路也走了彎路,我還想著構建一個MatchParent的MeasureSpec,然後傳給父控制元件(GridView、RecyclerView)用於測量呢。 
正確思路是,取第一個子View,即ContentView的寬度用作本控制元件的寬度即可,這樣在layout側滑選單時,自然而然將側滑選單layout在了不可見的區域,只有通過滑動才能顯示它。 
程式碼也沒啥好說的: 
onMeasure()中:


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //2016 11 09 add,適配GridLayoutManager,將以第一個子Item(即ContentItem)的寬度為控制元件寬度
        int contentWidth = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getVisibility() != GONE) {
                measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
                mHeight = Math.max(mHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                if (i > 0) {//第一個佈局是Left item,從第二個開始才是RightMenu
                    mRightMenuWidths += childView.getMeasuredWidth();
                } else {
                    contentWidth = childView.getMeasuredWidth();
                }
            }
        }
        setMeasuredDimension(contentWidth, mHeight);//寬度取第一個Item(Content)的寬度
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

onLayout()中,順序layoutchildView即可。