Android基础控件——RecyclerView实现窗口拖动和屏幕边缘吸铁石的效果

前言

  • 程序员:你看我写的RecyclerView多美呀,看看这颜色搭配多好看
  • (突然有天)产品:把这个列表给我拖动起来,记得加个动画让他贴边
  • 程序员(华丽的辞藻):那我走?
  • 程序员:这RecyclerView业务遗留太久,改起来有点难度,5天工作量吧
  • 产品(丰富的中国传统文化):那我走?

效果如下

在这里插入图片描述

主要功能

  • 长按能拖动
  • 列表的拖动和长按拖动不冲突
  • 松手后能贴边

实现

整个方案实现原理最难在于拖动时候的处理:

  1. 如何判断长按后,然后开始拖动
  2. 长按的边界判断,不让界面拖出设置的边界
  3. 贴边动画实现

1、定义布局

在实际开发中,不仅只是拖动个RecyclerView就完事了,随着业务的增加,在RecyclerView上可能还有其他业务的布局,这里也是模仿实际业务,拖动的是整个父布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#50ffffff"
        tools:context=".MainActivity">
    
    <!--拖动的父布局-->
    <FrameLayout
            android:id="@+id/drag_view_root"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        <!--父布局跟着动,他就会跟着走-->
        <com.example.uitest.recylerview.DragRecyclerView
                android:id="@+id/rv_drag"
                android:layout_width="72dp"
                android:layout_height="120dp"
                android:background="#5000ff00" />
        <!--依附在列表上的其他业务,也需要跟着走-->
        ......
    </FrameLayout>
</FrameLayout>

2、定义变量

class DragRecyclerView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {

    var TAG = "DragRecyclerView"

    //总开关
    private var isOpen = false

    private var screenWidth = 0
    private var screenHeight = 0

    //计算用
    private var lastLeft = 0F
    private var lastTop = 0F
    private var lastX = 0F
    private var lastY = 0F

    //拖动事件
    var isDownStart: (() -> Unit)? = null
    var isDragStart: (() -> Unit)? = null
    var isDragUp: (() -> Unit)? = null

    //真正拖动的父布局
    private var dragView: View? = null
    //是否拖动
    private var drag = false

    //限制拖动的区域
    @Volatile
    private var leftLimit = 0F
    @Volatile
    private var rightLimit = 0F
    @Volatile
    private var topLimit = 0F
    @Volatile
    private var bottomLimit = 0F

    //默认在有右边
    var isLeft = false
    //默认在竖屏
    private var isLandScape = false

    //长按事件
    var longPressRunnable: Runnable = Runnable {
        Log.i(TAG, "长按")
        drag = true
        isDragStart?.invoke()
    }

    init {
        screenWidth = resources.displayMetrics.widthPixels
        screenHeight = resources.displayMetrics.heightPixels
    }

    //设置拖动的父布局
    fun setDragView(view: View?) {
        this.dragView = view
    }

    //设置拖动的开关
    fun setDragOpen(isOpen: Boolean) {
        this.isOpen = isOpen
    }

    //设置拖动限制拖动的区域(竖屏)
    fun setProperticLimitRect(l: Float, t: Float, r: Float, b: Float) {
        Log.i(TAG, "setProperticLimitRect l=$l t=$t r=$r b=$b")
        isLandScape = false
        leftLimit = l
        rightLimit = screenWidth - r
        topLimit = t
        bottomLimit = screenHeight - b
    }

    //设置拖动限制拖动的区域(横屏)
    fun setLandScapeLimitRect(l: Float, t: Float, r: Float, b: Float) {
        Log.i(TAG, "setLandScapeLimitRect l=$l t=$t r=$r b=$b")
        isLandScape = true
        leftLimit = l
        rightLimit = screenHeight - r
        topLimit = t
        bottomLimit = screenWidth - b
        invalidate()
    }
}

3、长按逻辑判断

长按依赖的是用户手指点下之后,在短时间内,没有抬起来,则会执行长按的Runnable,表示用户长按拖动,当然这个方案不是很完美,这里的缺陷是,如果用户在滑动列表超过长按时长,也会被认为是长按拖动,如果追求完美的话,就是需要增加一个判断,判断这段时间内,用户在滑动事件中,是否已经达到列表滑动的标准或者是长按的标准

override fun dispatchTouchEvent(e: MotionEvent): Boolean {
    if (isOpen) {
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {
                postDelayed(longPressRunnable, ViewConfiguration.getLongPressTimeout().toLong())
            }
            MotionEvent.ACTION_MOVE -> {
                //如果追求完美,需要在此通过判断dx和dy,是否达到阈值,太小则表示长按,太大,就表示用户在列表滑动
            }
            MotionEvent.ACTION_UP -> {
                removeCallbacks(longPressRunnable)
            }
        }
    }
    return super.dispatchTouchEvent(e)
}

4、拖动和贴边实现

  1. 拖动时候计算拖动距离
  2. 拖动时候让原先的父布局移动
  3. 拖动时候计算边缘位置
  4. 松手后贴边处理

同样这里有个缺陷,由于底部边缘有虚拟导航栏,对于底部的限制区域,需要判断有导航栏显示的时候需要减去这部分的高度

override fun dispatchTouchEvent(e: MotionEvent): Boolean {
    if (isOpen) {
        when (e.action) {
            MotionEvent.ACTION_DOWN -> {

                isDownStart?.invoke()

                dragView?.let {
                    lastLeft = it.x.toFloat()
                    lastTop = it.y.toFloat()
                }

                lastX = e.rawX
                lastY = e.rawY

                drag = false
                postDelayed(longPressRunnable, ViewConfiguration.getLongPressTimeout().toLong())
            }
            MotionEvent.ACTION_MOVE -> {
                dragView?.let {
                    //计算拖动距离
                    val dx = e.rawX - lastX
                    val dy = e.rawY - lastY
                    //让原先的父布局移动的位置
                    var left: Float = (lastLeft + dx).toFloat()
                    var top: Float = (lastTop + dy).toFloat()
                    //限制拖动范围
                    if (top < topLimit) {
                        top = topLimit.toFloat()
                    }
                    if (top + height > bottomLimit) {
                        top = (bottomLimit - height).toFloat()
                    }
                    if (left < leftLimit) {
                        left = leftLimit.toFloat()
                    }
                    if (left + width > rightLimit) {
                        left = (rightLimit - width).toFloat()
                    }

                    Log.i(TAG, "left=$left top=$top")
                    if (drag) {
                        dragView?.x = left.toFloat()
                        dragView?.y = top.toFloat()
                        return true
                    }
                }
            }
            MotionEvent.ACTION_UP -> {
                if (drag) {
                    dragView?.let {
                        val center = if (isLandScape) screenHeight / 2 else screenWidth / 2
                        val currentLeft = width / 2 + it.x
                        if (center - currentLeft > 0) {
                            isLeft = true
                            //向左贴边
                            it.animate()
                                .setInterpolator(AccelerateInterpolator())
                                .setDuration(300)
                                .x(leftLimit.toFloat())
                                .setListener(object : Animator.AnimatorListener {
                                    override fun onAnimationRepeat(animation: Animator?) {
                                    }

                                    override fun onAnimationCancel(animation: Animator?) {
                                    }

                                    override fun onAnimationStart(animation: Animator?) {
                                    }

                                    override fun onAnimationEnd(animation: Animator?) {
                                        dragView?.let {
                                            lastLeft = it.x.toFloat()
                                            lastTop = it.y.toFloat()
                                            Log.i(TAG, "lastLeft=$lastLeft lastTop=$lastTop")
                                        }
                                    }
                                })
                                .start()
                        } else {
                            isLeft = false
                            //向右贴边
                            it.animate()
                                .setInterpolator(AccelerateInterpolator())
                                .setDuration(300)
                                .x(rightLimit - width.toFloat())
                                .setListener(object : Animator.AnimatorListener {
                                    override fun onAnimationRepeat(animation: Animator?) {
                                    }

                                    override fun onAnimationCancel(animation: Animator?) {
                                    }

                                    override fun onAnimationStart(animation: Animator?) {
                                    }

                                    override fun onAnimationEnd(animation: Animator?) {
                                        dragView?.let {
                                            lastLeft = it.x.toFloat()
                                            lastTop = it.y.toFloat()
                                            Log.i(TAG, "lastLeft=$lastLeft lastTop=$lastTop")
                                        }
                                    }
                                })
                                .start()
                        }
                    }
                }
                drag = false
                removeCallbacks(longPressRunnable)
                isDragUp?.invoke()
            }
        }
    }
    return super.dispatchTouchEvent(e)
}

使用

  1. 实现Recycler列表
  2. 打开拖动开关
  3. 设置拖动限制范围
  4. 设置拖动事件回调
class MainActivity : AppCompatActivity() {

    var TAG = "MainActivity"
    var drag_view: DragRecyclerView? = null
    var drag_view_root: FrameLayout? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        drag_view_root = findViewById(R.id.drag_view_root)
        drag_view = findViewById(R.id.rv_drag)
        
        drag_view?.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        drag_view?.adapter = object : RvAdapter(this@MainActivity) {}
        drag_view?.setDragOpen(true)
        drag_view?.setProperticLimitRect(20f, 20f, 20f, 20f)
        drag_view?.setDragView(drag_view_root)
        drag_view?.isDragStart = {
            Log.i("Hensen", "[拖动开始]")
        }
        drag_view?.isDownStart = {
            Log.i("Hensen", "[手指点下]")
        }
        drag_view?.isDragUp = {
            Log.i("Hensen", "[手指抬起]")
        }
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许英俊潇洒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值