Android实现一个 左滑显示删除按钮的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 一样就好了,详细就不写了

以下是基于 Kotlin 和 RecyclerView 实现左滑显示删除按钮删除 item 的完整代码实现: 首先,我们需要在 app 的 build.gradle 文件中添加 RecyclerView 和 Material Design 的依赖: ```groovy dependencies { implementation "com.google.android.material:material:1.4.0" implementation "androidx.recyclerview:recyclerview:1.2.1" } ``` 接着,在 activity 的布局文件中添加 RecyclerView: ```xml <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layoutManager="LinearLayoutManager" /> ``` 然后,我们需要定义 RecyclerView 的 Adapter 和 ViewHolder: ```kotlin class MyAdapter(private val items: List<String>, private val listener: (Int) -> Unit) : RecyclerView.Adapter<MyAdapter.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_layout, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(items[position], listener) } override fun getItemCount(): Int { return items.size } class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val textView: TextView = itemView.findViewById(R.id.textView) private val deleteButton: ImageButton = itemView.findViewById(R.id.deleteButton) fun bind(item: String, listener: (Int) -> Unit) { textView.text = item deleteButton.setOnClickListener { listener(adapterPosition) } } } } ``` 其中,ViewHolder 中有一个 deleteButton,我们将在左滑显示它。 接下来,我们需要在 activity 中设置 RecyclerView 的 Adapter 和 ItemTouchHelper,实现左滑显示删除按钮删除 item 的功能: ```kotlin class MainActivity : AppCompatActivity() { private lateinit var recyclerView: RecyclerView private lateinit var adapter: MyAdapter private lateinit var items: MutableList<String> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) items = mutableListOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5") adapter = MyAdapter(items) { position -> items.removeAt(position) adapter.notifyItemRemoved(position) } recyclerView = findViewById(R.id.recyclerView) recyclerView.adapter = adapter val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback( 0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT ) { override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val position = viewHolder.adapterPosition if (direction == ItemTouchHelper.LEFT) { items.removeAt(position) adapter.notifyItemRemoved(position) } else { // do something on right swipe } } override fun onChildDraw( c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean ) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { val itemView = viewHolder.itemView val deleteIcon = ContextCompat.getDrawable( this@MainActivity, R.drawable.ic_delete ) val iconMargin = resources.getDimension(R.dimen.icon_margin).toInt() val iconTop = itemView.top + (itemView.height - deleteIcon!!.intrinsicHeight) / 2 val iconBottom = iconTop + deleteIcon.intrinsicHeight if (dX > 0) { val iconLeft = itemView.left + iconMargin val iconRight = itemView.left + iconMargin + deleteIcon.intrinsicWidth deleteIcon.setBounds(iconLeft, iconTop, iconRight, iconBottom) c.clipRect(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom) deleteIcon.draw(c) } else { val iconLeft = itemView.right - iconMargin - deleteIcon.intrinsicWidth val iconRight = itemView.right - iconMargin deleteIcon.setBounds(iconLeft, iconTop, iconRight, iconBottom) c.clipRect(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom) deleteIcon.draw(c) } } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) } }) itemTouchHelper.attachToRecyclerView(recyclerView) } } ``` 在 ItemTouchHelper 的 onChildDraw 方法中,我们根据滑动的方向和距离画出了删除按钮的图标,并通过 clipRect 方法限制了按钮显示范围。 最后,我们在 res/drawable 目录下创建了一个名为 ic_delete 的图标,用于显示删除按钮。 完整代码实现请参考以下 GitHub 仓库:https://github.com/linroid/RecyclerViewSwipeToDelete
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值