recycleview中item的側滑擴展優化與項目中的具體應用

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

之前的文章只是實現了item的側滑,但在項目中需要添加滑動的優化以及以後的擴展

 

分析:

  1. 滑動優化是指從部分展開過度到安全展開,添加動畫機制或者使用Scroller控制滑動,使其不那麼生硬
  2. 添加移動速度,進行展開判斷
  3. 提供定製的item,封裝菜單view與內容view,各個部位的點擊

一、滑動優化

這裏本來想借助Scroller來控制滑動,但是發現關閉過程中打開,有衝突。想到了需要藉助item裏的的Scroller,進而更新。而不是recycleview的Scroller。這樣比較繁瑣,以此放棄採用動畫(ObjectAnimator)。有興趣的朋友可以嘗試下Scroller。

ObjectAnimator採用默認的插值器,因scrollTo方法的參數是Int類型,以此如下:

    private val objectUpdateListener = object : ValueAnimator.AnimatorUpdateListener {
        override fun onAnimationUpdate(animation: ValueAnimator) {
            if (animation is ObjectAnimator) {
                var target = animation.target as View
                var slide = animation.animatedValue as Int
                target.scrollTo(slide, 0)
            }
        }

    }
    private val closeAnimator: ObjectAnimator by lazy {
        ObjectAnimator.ofInt(this@ItemSlideRecycleView, "slideClose", 0, 0)
            .apply {
                duration = 200
                addUpdateListener(objectUpdateListener)
            }
    }
    private val openAnimator: ObjectAnimator by lazy {
        ObjectAnimator.ofInt(this@ItemSlideRecycleView, "slideOpen", 0, 0)
            .apply {
                duration = 200
                addUpdateListener(objectUpdateListener)
            }
    }
    private fun startCloseAnimator(view: View, start: Int, end: Int) {
        closeAnimator.target = view
        closeAnimator.setIntValues(start, end)
        closeAnimator.start()
    }

    private fun startOpenAnimator(view: View, start: Int, end: Int) {
        openAnimator.target = view
        openAnimator.setIntValues(start, end)
        openAnimator.start()
    }

修改打開與關閉菜單方法:

    /**
     * 菜單展開
     */
    private fun showMenu(view: View) {
        startOpenAnimator(view, view.scrollX, mMenuWidth)
        mMenuShowAllTag = true
    }

    /**
     * 菜單關閉
     */
    private fun closeMenu(view: View) {
        if (closeAnimator.isRunning) {
            closeAnimator.cancel()
            (closeAnimator.target as View).scrollTo(0, 0)
        }
        startCloseAnimator(view, view.scrollX, 0)
        Log.e(TAG, "closeMenu: ${view.hashCode()}")
        mMenuShowAllTag = false
    }

二、添加手指滑動速度判斷

當手指的移動速度大於一定值時,也認爲是打開菜單的意圖。所以在checkEffectiveSlideLength()添加判斷。

1.首先需要初始化相關類VelocityTracker

    private val mVelocityTracker: VelocityTracker by lazy {
        VelocityTracker.obtain()
    }

2.添加速度判斷

    /**
     * 最小有效滑動速度
     * 向左滑,速度爲負數
     */
    private val MIN_SPEED = -400

    /**
     * 檢測滑動速度是否符合我們的要求
     */
    private fun checkEffectiveSpeed(): Boolean {
        mVelocityTracker.computeCurrentVelocity(1000)
        return mVelocityTracker.xVelocity <= MIN_SPEED
    }

3.我們需要在onInterceptTouchEvent方法處理ACTION_MOVE事件以及onTouchEvent方法處理ACTION_UP事件添加速度判斷。因爲一個決定是否攔截此事時間,一個判斷是否展開菜單,都需要速度的判斷。

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        mVelocityTracker.addMovement(e)
        when (e.action) {
            /*代碼省略*/
            MotionEvent.ACTION_MOVE -> {
                if (checkEffectiveSlide(e.x, e.y)) {
                    //查找當前菜單
                    findMotionView(downLastX.toInt(), downLastY.toInt())
                    slideEffiectiveTag = true
                    return true
                }
            }
            /*代碼省略*/
            
        }
        var onInterceptTouchEvent = super.onInterceptTouchEvent(e)
        Log.e(TAG, "onInterceptTouchEvent: result=$onInterceptTouchEvent")
        return onInterceptTouchEvent
    }
    /**
     * 檢測滑動是否符合我們的要求
     */
    private fun checkEffectiveSlide(x: Float, y: Float): Boolean {
        if (checkEffectiveSpeed()) {
            return true
        }
        var changeX = lastX - x
        var changeY = lastY - y
        if (changeX > mMinSlide && changeX > Math.abs(changeY)) {//水平向右滑動
            return true
        }
        return false
    }
    override fun onTouchEvent(e: MotionEvent): Boolean {
        mVelocityTracker.addMovement(e)
        when (e.action) {
            /*代碼省略*/
            MotionEvent.ACTION_UP -> {
                Log.e(TAG, "onTouchEvent: ACTION_UP $slideEffiectiveTag")
                //此標誌恢復初始值
                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
    }
    /**
     * 手指擡起,對目標view進行最後的移動
     * 即決定菜單是否展開或關閉
     */
    private fun upFinalMoveToMenuView() {
        mMenuView?.let {
            if (it.scrollX >= mMenuWidth / 2f || checkEffectiveSpeed()) {
                showMenu(it)
            } else {
                closeMenu(it)
            }
        }
    }

三、封裝內容與菜單以及點擊

這個說什麼好呢,好吧,先定義recycleview的適配器抽象類(ItemSlideAdapter),然後抽離相關方法。

 

1.指定容器viewgroup把內容和菜單分離。由適配器進行添加,這裏使用了LinearLayout,如下:

    private fun createView(context: Context, contentView: View, menuView: View): ViewGroup {
        return LinearLayout(context).apply {
            layoutParams = RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.MATCH_PARENT,
                RecyclerView.LayoutParams.WRAP_CONTENT
            )
            orientation = LinearLayout.HORIZONTAL
            addView(contentView)
            var layoutParams = menuView.layoutParams
            if (null == layoutParams || layoutParams.width <= 0) {
                throw RuntimeException("getMenuView方法得到的view,必須設置固定的寬度")
            }
            addView(menuView)
        }
    }

2.定義內容Holder(ContentViewHolder)與菜單holder(MenuViewHolder)與內容view和菜單view進行綁定,有點類似listview的ViewHolder。這將在其子類實現。如下:

    abstract class ContentViewHolder(val view: View)
    abstract class MenuViewHolder(val view: View)

3.構造生成ContentViewHolder與MenuViewHolder抽象方法,提供viewType參數是爲了擴展。

    //C : ItemSlideAdapter.ContentViewHolder, M : ItemSlideAdapter.MenuViewHolder    
    abstract fun getMenuViewHolder(parent: ViewGroup, viewType: Int): M
    abstract fun getContentViewHolder(parent: ViewGroup, viewType: Int): C

4.創建RecyclerView.ViewHolder的實現類。因爲已經將內容和菜單分離下去了,item成了一個空殼,所以可以將其確定。但要包含ContentViewHolder和MenuViewHolder,如下:

    class ItemSlideViewHolder(
        view: ViewGroup,
        var contentViewHolder: ContentViewHolder,
        var menuViewHolder: MenuViewHolder
    ) : RecyclerView.ViewHolder(view)

5.這樣的話就可以onCreateViewHolder方法實現了,如下:

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemSlideAdapter.ItemSlideViewHolder {
        var contentViewHolder = getContentViewHolder(parent, viewType)
        var menuViewHolder = getMenuViewHolder(parent, viewType)
        return ItemSlideAdapter.ItemSlideViewHolder(
            createView(
                parent.context,
                contentViewHolder.view,
                menuViewHolder.view
            ), contentViewHolder, menuViewHolder
        )
    }

6.點擊的封裝,想了一下這個還是不加了吧,在抽象類的實現類裏自行加入。因爲菜單子view的數量不定,還有內容子view的數量也不定,哪些需要點擊也不定。除非都已確定,非要寫的話,需要添加好多方法,得不償失。

7.因此抽象類的全部代碼,如下

package com.xinheng.leftslidedeleterecycleview

import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import java.lang.RuntimeException

/**
 * Created by XinHeng on 2019/03/07.
 * describe:
 */
abstract class ItemSlideAdapter<C : ItemSlideAdapter.ContentViewHolder, M : ItemSlideAdapter.MenuViewHolder> :
    RecyclerView.Adapter<ItemSlideAdapter.ItemSlideViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemSlideAdapter.ItemSlideViewHolder {
        var contentViewHolder = getContentViewHolder(parent, viewType)
        var menuViewHolder = getMenuViewHolder(parent, viewType)
        return ItemSlideAdapter.ItemSlideViewHolder(
            createView(
                parent.context,
                contentViewHolder.view,
                menuViewHolder.view
            ), contentViewHolder, menuViewHolder
        )
    }

    override fun onBindViewHolder(holder: ItemSlideViewHolder, position: Int) {
        var contentViewHolder = holder.contentViewHolder
        var menuViewHolder = holder.menuViewHolder
        onBindHolder(contentViewHolder, menuViewHolder, position)
    }

    private fun createView(context: Context, contentView: View, menuView: View): ViewGroup {
        return LinearLayout(context).apply {
            layoutParams = RecyclerView.LayoutParams(
                RecyclerView.LayoutParams.MATCH_PARENT,
                RecyclerView.LayoutParams.WRAP_CONTENT
            )
            orientation = LinearLayout.HORIZONTAL
            addView(contentView)
            var layoutParams = menuView.layoutParams
            if (null == layoutParams || layoutParams.width <= 0) {
                throw RuntimeException("getMenuView方法得到的view,必須設置固定的寬度")
            }
            addView(menuView)
        }
    }

    abstract fun onBindHolder(contentViewHolder: ContentViewHolder, menuViewHolder: MenuViewHolder, position: Int)
    abstract fun getMenuViewHolder(parent: ViewGroup, viewType: Int): M
    abstract fun getContentViewHolder(parent: ViewGroup, viewType: Int): C
    class ItemSlideViewHolder(
        view: ViewGroup,
        var contentViewHolder: ContentViewHolder,
        var menuViewHolder: MenuViewHolder
    ) : RecyclerView.ViewHolder(view)

    abstract class ContentViewHolder(val view: View)
    abstract class MenuViewHolder(val view: View)
}

8.提供一個例子,實現了多種菜單與多種內容搭配。GitHub項目地址,效果如下圖:

圖片待更新

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