Android实现一个 左滑显示删除按钮的RecyclerView

本文详细介绍了一种在Android中实现左滑显示删除按钮的RecyclerView的方法。通过精细控制RecyclerView的事件分发,使得横向滑动可以展示删除按钮,同时保持竖直方向的滚动功能不变。文章提供了完整的代码实现,包括如何处理触摸事件、动画效果以及滑动范围的计算。

Android实现一个 左滑显示删除按钮的RecyclerView

思路整理

要实现这么个效果,主要就是对RecyclerView的事件分发整理,横向移动传递给view,竖直方向由Recyclerview整理 ,点击事件原封不动传递

参考效果

主要参考了手机qq 的消息效果,简单来说 左滑显示删除按钮,按任意其他位置还原

实现

具体思路拆分比较麻烦,直接贴代码,基本上看注释就差不多了

package com.jkys.common_ui_widget

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.animation.OvershootInterpolator

class LeftWipeRecyclerView : RecyclerView {
    //滚动动画,之所以不用Scroller,是因为Scroller 需要配合ondraw方法使用,而我只需要对child view scroll
    var scroller: ValueAnimator

    var touchSlop = 0 //最小的滑动单位,超过这个值才被认为是有效的滑动  系统值

    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
        //定义回弹效果的动画
        scroller = ValueAnimator()
        scroller.setInterpolator(OvershootInterpolator(5f))
        layoutManager = LinearLayoutManager(context)

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    override fun requestLayout() {
        super.requestLayout()
        if (mSwipeTargetView != null) {
            mSwipeTargetView?.scrollTo(0, 0)
            mSwipeTargetView = null
            isFirstCheckMoved = false
            isShouldCheckTouch = true
        }
    }

    override fun setLayoutManager(layout: LayoutManager?) {
        if (layout is LinearLayoutManager) {
            super.setLayoutManager(layout)
        } else {
            super.setLayoutManager(LinearLayoutManager(context))

        }
    }


    var speed = 1 // 回弹的速度系数

    //是否进行过水平方向移动
    //也就是基于这个判断touch event up 是否要传递给child 触发点击
    var isHorScrolled = false

    //滑动的View
    var mSwipeTargetView: View? = null

    //判断是否为第一次收到有效的touch Move
    //避免奇怪的滑动
    var isFirstCheckMoved: Boolean = false

    //如果进行了y轴方向的滚动,就不处理事件分发,
    // 交由普通RecyclerView的处理
    var isShouldCheckTouch: Boolean = true

    var lastPos = FloatArray(2)//记录上一个touch点坐标

    var scrollRange = 0 //记录滑动范围


    var isClickOtherItem = false //显示隐藏区域时 是否点击了其他位置


    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        //如果在滚动动画中 不接受任何touch事件
        if (isScrolling) return true

        when (ev?.action) {
            MotionEvent.ACTION_DOWN -> {
                //down事件初始化
                isClickOtherItem = false
                isHorScrolled = false
                isFirstCheckMoved=false
                lastPos[0] = ev?.x
                lastPos[1] = ev?.y

                //判断如果有 滑动区域显示,这个时候touch
                // 漏出的View有效
                //其他的View touch事件拦截,将露出的View 滚回初始状态
                if (mSwipeTargetView != null) {
                    var clickRect = Rect()
                    mSwipeTargetView!!.getHitRect(clickRect)

                    if (!clickRect.contains(ev?.x.toInt(), ev?.y.toInt())) {
                        scrollToIdle(true)
                        isFirstCheckMoved = false
                        isShouldCheckTouch = true
                        //isClickOtherItem 设置为true 拦截事件传递
                        //{@link #onInterceptTouchEvent}
                        isClickOtherItem = true

                    }
                    return super.dispatchTouchEvent(ev)

                }


            }
            MotionEvent.ACTION_MOVE -> {
                if (isShouldCheckTouch) {
                    //检查是否是滚动的最小单位
                    if (Math.hypot(ev.x.toDouble() - lastPos[0], ev.y.toDouble() - lastPos[1]) > touchSlop) {
                        //是否是第一次滚动
                        //这样避免在recylerview 本身滚动的同时左右滑动手指引起奇怪的判断和View状态
                        if (!isFirstCheckMoved) {
                            //如果满足条件 检查是否是x方向还是y方向滚动
                            if (checkScollHorizon(ev.x, ev.y)) {
                                //第一次滚动 为水平方向滚动
                                //那么就找到并赋值当前的滑动View
                                if (mSwipeTargetView == null) {
                                    mSwipeTargetView = findItemByPostion(lastPos[0].toInt(), lastPos[1].toInt())
                                }
                            }
                            isFirstCheckMoved = true
                        }

                        if (mSwipeTargetView != null) {
                            //横向滑动超过一定水平,接下来的点击事件就不要了
                            if (Math.abs(ev.x.toDouble() - lastPos[0]) > touchSlop) {
                                isHorScrolled = true
                            }

                            if (scrollHor(ev)) {
                                return true
                            }
                        } else {
                            //没有滑动目标,那么就不再处理move 事件,
                            //交给recyclerview父类方法处理事件分发
                            lastPos[0] = ev.x
                            lastPos[1] = ev.y
                            isShouldCheckTouch = false
                        }
                    } else {
//                        滑动距离小于滑动判断临界
//                        为了效果流程,满足 x方向,并且滑动view不为Null,就横向滑动
                        return scrollHor(ev)
                    }
                }
            }
            MotionEvent.ACTION_UP -> {

//                松手时
                isShouldCheckTouch = true
                if (mSwipeTargetView != null) {

                    //滑动view滚动到指定位置
                    // 默认 OR 显示全部隐藏视图
                    scrollToIdle()

                    //如果水平点击过 事件不向下传递
                    if (isHorScrolled) {
                        isHorScrolled = false
                        return true
                    }
                }


            }
        }
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {

        // isClickOtherItem 是否拦截事件传递
        return super.onInterceptTouchEvent(e) || isClickOtherItem
    }


    //有滑动view后,滑动view跟随手指移动,要注意的是
    // 手指左滑,ev?.x - lastPos[0]<0
    // 而 要显示右边的区域 scrollx 要 >0
    //所以 mSwipeTargetView!!.scrollX - scrollvalue
    //即左滑显示右边
    private fun scrollHor(ev: MotionEvent?): Boolean {
        if (ev == null) return false
        if (mSwipeTargetView != null) {

            if (checkScollHorizon(ev?.x, ev?.y)) {
                var scrollvalue = (ev?.x - lastPos[0]).toInt()
                var scrolltarget = mSwipeTargetView!!.scrollX - scrollvalue
                if (scrolltarget >= scrollRange) {
                    scrolltarget = scrollRange
                } else if (scrolltarget < 0) {
                    scrolltarget = 0
                }
                mSwipeTargetView?.scrollTo(scrolltarget, 0)
            }
            lastPos[0] = ev?.x
            lastPos[1] = ev?.y
        }
        return true
    }

    //检查是否是x方向  判断依据 x方向是y方向的两倍
    fun checkScollHorizon(x: Float, y: Float): Boolean {
        return Math.abs(y - lastPos[1]) * 2 < Math.abs(x - lastPos[0])
    }

    //查找到点击的view
    //遍历 所以的child 的 hitrect
    //找到满足坐标在里面的view
    fun findItemByPostion(x: Int, y: Int): View? {


        var firstchildIndex = (layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
        if (firstchildIndex < 0) {
            return null
        }

        val count = getChildCount();
        val itemRect = Rect()
        for (i in 0 until count) {
            val child = getChildAt(i);
            if (child != null && child.getVisibility() == View.VISIBLE) {
                child.getHitRect(itemRect);
                if (itemRect.contains(x, y)) {

                    //viewholderitem 需要标示隐藏区域的id为 R.id.scrollable,这样定位隐藏区域的width
                    scrollRange = child.findViewById<View>(R.id.scrollable).width

                    return child
                }
            }
        }
        return null
    }


    private var isScrolling: Boolean = false

    //滑动的view 松手或者点击别的区域 滚动到合适的位置
    //需要注意 如果是拖回起初位置时,手动释放掉view
    fun scrollToIdle(forceBase: Boolean = false) {
        if (mSwipeTargetView != null) {
            var isBackToZero = forceBase || scrollRange > mSwipeTargetView!!.scrollX * 2
            var scrollTarget = if (isBackToZero) 0 else scrollRange
            if (mSwipeTargetView!!.scrollX == scrollTarget) {
                if (scrollTarget == 0) {
                    mSwipeTargetView = null
                }
                return
            }
            scroller.setIntValues(mSwipeTargetView!!.scrollX, scrollTarget)
            scroller.addUpdateListener {
                mSwipeTargetView?.scrollTo(it.animatedValue as Int, 0)
            }
            scroller.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator?) {
                    mSwipeTargetView?.scrollTo(scrollTarget, 0)
                    if (scrollTarget == 0) {
                        mSwipeTargetView = null
                    }
                    scroller?.removeAllUpdateListeners()
                    scroller?.removeAllListeners()
                    isScrolling = false
                }
            })
            scroller.setDuration(Math.abs(scrollRange) / 2 * speed.toLong())
            scroller.start()
            isScrolling = true
        }
    }

    //释放资源
    override fun onDetachedFromWindow() {
        mSwipeTargetView=null
        scroller?.cancel()
        scroller?.removeAllUpdateListeners()
        scroller?.removeAllListeners()
        super.onDetachedFromWindow()

    }


}

使用

使用的话 记得item 的layout 的滑出区域 要 id= R.id.scrollable,别的和普通的recyclerView 一样就好了,详细就不写了

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值