recycleview的側滑刪除

今天介紹下項目中用到的側滑刪除

具體步驟:

1.recycleview垂直方向滑動,保證recycleview的item必須爲viewgroup,並且item佈局中的菜單view必須在最右邊(項目中默認向左滑動有效),出可見屏幕外,指定具體的寬度。LayoutManager採用LinearLayoutManager(也可用GridLayoutManager,只要是垂直方向就行)。item佈局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:orientation="horizontal"
        android:layout_height="60dp">
    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:orientation="vertical">

        <TextView
                android:id="@+id/tv_content"
                android:layout_width="match_parent"
                android:layout_height="59dp"
                android:textSize="15sp"
                android:text="第幾個"
                android:gravity="center"/>
        <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                android:background="@color/colorPrimaryDark"/>
    </LinearLayout>
    <TextView
            android:id="@+id/tv_delete"
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:textSize="15sp"
            android:gravity="center"
            android:text="刪除"
            android:textColor="@android:color/white"
            android:background="@color/colorAccent"/>
</LinearLayout>

2.準備工作已好,接下來主要是對手勢的監聽: 

  1. 手指按下,獲取當前子view(含菜單);關閉之前已打開的view(菜單)
  2. 手指移動,移動當前view,顯示隱藏的菜單
  3. 手指擡起,判斷是否顯示或隱藏菜單
  4. 隱藏的菜單完全顯示後,添加點擊監聽與回調

首先需要自定義RecycleView;之後子view用菜單進行代替;下面開始一一分析。

一、前三步都是對事件的處理,因此放在一起進行說明。在onInterceptTouchEvent()方法進行攔截和onTouchEvent()方法處理事件。

1.onInterceptTouchEvent()方法

①ACTION_DOWN

不能對ACTION_DOWN進行攔截(子view可能需要點擊),可以進行座標記錄,以及關閉非上一個已打開的菜單。

         MotionEvent.ACTION_DOWN -> {
                //關閉上一個菜單
                closeNowMenu(e.x.toInt(), e.y.toInt())
                //記錄座標值
                updateLastXY(e.x, e.y)
            }

     private fun closeNowMenu(x: Int, y: Int) {
        if (null != mMenuView && mMenuShowAllTag) {
            if (!isNowMenu(x, y)) {
                closeMenu(mMenuView!!)
                mMenuShowAllTag = false
                slideEffiectiveTag = false
            }
        }
    }
    private fun updateDownLastXY(x: Float, y: Float) {
        downLastX = x
        downLastY = y
    }

    private fun updateLastXY(x: Float, y: Float) {
        lastX = x
        lastY = y
    }

②ACTION_MOVE

進行是否爲有效移動判斷,進行酌情攔截,哈哈。。。(見checkEffectiveSlideLength()方法)添加標誌位slideEffiectiveTag,標識此後所有的事件都和移動菜單有關進行攔截。並且需要確定當前觸摸的item(含菜單)mMenuView,( findMotionView()方法,見代碼,抄自Android的ABListView的方法,做了些修改)

    private fun findMotionView(x: Int, y: Int) {
        //檢查是否爲當前菜單
        if (isNowMenu(x, y)) {
            return
        }
        val frame = mTouchFrame
        for (i in childCount - 1 downTo 0) {
            val child = getChildAt(i)
            if (child == mMenuView) {
                continue
            }
            if (child.visibility == View.VISIBLE) {
                child.getHitRect(frame)
                if (frame.contains(x, y)) {
                    //當前觸碰的view
                    mMenuView = child as ViewGroup
                }
            }
        }
    }

    /**
     * 是否爲當前菜單
     */
    private fun isNowMenu(x: Int, y: Int): Boolean {
        mMenuView?.let {
            it.getHitRect(mTouchFrame)
            if (mTouchFrame.contains(x, y)) {
                return true
            }
        }
        return false
    }

③ACTION_UP

若slideEffiectiveTag爲true則直接攔截。(之後一定要注意這個標誌的恢復初始化,我就是忘了,導致菜單的點擊事件無法響應,查了原因才知道ACTION_UP被攔截了,導致事件在傳遞過程中變成了ACTION_CANCEL)

   override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {
                //關閉上一個菜單
                closeNowMenu(e.x.toInt(), e.y.toInt())
                //記錄座標值
                updateLastXY(e.x, e.y)
            }
            MotionEvent.ACTION_MOVE -> {
                var slide = checkEffectiveSlideLength(e.x, e.y)
                if (slide > 0f) {
                    //查找當前菜單
                    findMotionView(downLastX.toInt(), downLastY.toInt())
                    slideEffiectiveTag = true
                    return true
                }
            }
            MotionEvent.ACTION_UP -> {
                if (slideEffiectiveTag) {
                    return true
                }
            }
        }
        var onInterceptTouchEvent = super.onInterceptTouchEvent(e)
        Log.e(TAG, "onInterceptTouchEvent: result=$onInterceptTouchEvent")
        return onInterceptTouchEvent
    }

2.onTouchEvent()方法

①ACTION_DOWN

執行此方法,說明子view沒有對事件進行消費,添加標誌位onTouchEventDownTag,用於當移動有效時獲取當前菜單

②ACTION_MOVE

進行是否爲有效移動判斷(checkEffectiveSlideLength()方法),這個有可能來自onInterceptTouchEvent對ACTION_MOVE的攔截或者onTouchEvent的ACTION_DOWN爲true。但是這一步都需要知道當前觸摸的菜單是哪一個。

如果onTouchEventDownTag爲true,則需要重新確認當前觸摸的item(含菜單)mMenuView。因爲onTouchEventDownTag爲true,說明事件ACTION_MOVE沒有經過onInterceptTouchEvent方法,則需要確認mMenuView。

然後進行移動操作(見moveToMenuView()方法),這裏藉助了scrollBy和scrollTo方法。這裏有一點需要注意下,當mMenuView水平滑動距離和手指的水平移動距離和大於mMenuView的寬度則直接完全展示菜單

若爲有效滑動或者當前菜單已展開,則消費此事件。還記得onInterceptTouchEvent方法中對ACTION_DOWN的處理麼(查看closeNowMenu()方法),噹噹前觸碰的菜單還是上一個時,沒有做關閉處理。因此此時遇到當前菜單展開,表示此事件還是和菜單滑動有關,所以進行消費。否則會導致recycleview響應上下滑動,有可能使已展開菜單滑出當前頁面。

            MotionEvent.ACTION_MOVE -> {
                //*****中間代碼忽略*****///
                if (slideEffiectiveTag || mMenuShowAllTag) {
                    return true
                }
            }
    /**
     * 移動當前view
     */
    private fun moveToMenuView(slide: Int) {
        mMenuView?.let {
            mMenuWidth = it.getChildAt(1).measuredWidth
            if (it.scrollX + slide >= mMenuWidth) {
                showMenu(it)
            } else {
                mMenuShowAllTag = false
                it.scrollBy(slide, 0)
            }
        }
    }

③ACTION_UP

恢復onTouchEventDownTag標誌位的初始值

若slideEffiectiveTag爲true即滑動有效。直接返回true,並且對mMenuView是否展開或隱藏進行判斷。這裏只是對mMenuView的露出部分進行了判斷;如果不小於mMenuView寬度的一半則顯示,否則隱藏。還有一點需要說明下,slideEffiectiveTag爲false時,但是有菜單完全展示了,需要將其關閉。這是防止點擊了item(含菜單)菜單之外的view,但其沒有對此事件進行消費。

    /**
     * 檢測有效的滑動距離
     * 即是否符合我們要求的滑動
     */
    private fun checkEffectiveSlideLength(x: Float, y: Float): Float {
        var changeX = lastX - x
        var changeY = lastY - y
        Log.e(TAG, "checkEffectiveSlideLength: changeX=$changeX changeY=$changeY  $mMinSlide")
        if (changeX > 0 && changeX > Math.abs(changeY)) {//水平向右滑動
            if (onTouchEventDownTag) {
                findMotionView(downLastX.toInt(), downLastY.toInt())
                //此標誌恢復初始值,已找到當前觸摸的菜單
                onTouchEventDownTag = false
            }
            return changeX
        }
        updateLastXY(x, y)
        return -1f
    }
    override fun onTouchEvent(e: MotionEvent): Boolean {
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {
                updateLastXY(e.x, e.y)
                Log.e(TAG, "onTouchEvent: ACTION_DOWN")
                //findMotionView(e.x.toInt(), e.y.toInt())
                onTouchEventDownTag = true
            }
            MotionEvent.ACTION_MOVE -> {
                Log.e(TAG, "onTouchEvent: ACTION_MOVE")
                var slide = checkEffectiveSlideLength(e.x, e.y)
                Log.e(TAG, "onTouchEvent: slide=$slide  $slideEffiectiveTag")
                if (slide > 0f) {
                    slideEffiectiveTag = true
                    moveToMenuView(slide.toInt())
                    return true
                }
                if (slideEffiectiveTag || mMenuShowAllTag) {
                    return true
                }
            }
            MotionEvent.ACTION_UP -> {
                Log.e(TAG, "onTouchEvent: ACTION_UP")
                //此標誌恢復初始值
                onTouchEventDownTag = false
                if (slideEffiectiveTag) {
                    if (!mMenuShowAllTag)
                        upFinalMoveToMenuView()
                    //此標誌恢復初始值
                    slideEffiectiveTag = false
                    return true
                } else {
                    //若沒有有效滑動,但已展開,則關閉菜單
                    if (mMenuShowAllTag) {
                        closeMenu()
                    }
                }
            }
        }
        var onTouchEvent = super.onTouchEvent(e)
        Log.e(TAG, "onTouchEvent: result=$onTouchEvent")
        return onTouchEvent
    }
    /**
     * 菜單展開
     */
    private fun showMenu(view: View) {
        view.scrollTo(mMenuWidth, 0)
        mMenuShowAllTag = true
    }

    /**
     * 菜單關閉
     */
    private fun closeMenu(view: View) {
        view.scrollTo(0, 0)
        Log.e(TAG, "closeMenu: ${view.hashCode()}")
        mMenuShowAllTag = false
    }

二、菜單的點擊回調

這個沒什麼好說的,直接在recycleview的adapter適配器中添加點擊監聽即可。將完成的自定義recycleview加入加項目中,查看效果,這些代碼就省略了,下面查看效果圖。需要注意的是點擊後別忘了調用closeMenu()方法。GitHub自定義recycleview代碼

至於其中的事件攔截與消費,本文不是重點。如有錯誤或指導,歡迎留言。

    fun closeMenu() {
        mMenuView?.let {
            closeMenu(it)
        }
    }

效果圖

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章