Android开发 拖放滑动DragShadowBuilder与OnDragListener使用讲解

观心静 / 2023-06-06 / 原文

前言

  在Android里实现View的拖放滑动无需自己去重写OnTouchListener,Android已经提供了DragShadowBuilder与OnDragListener来轻松的实现此类需求。

  当然还有一个ViewDragHelper类也能更简单方便的帮我们实现拖放滑动功能,但是DragShadowBuilder与OnDragListener的实现灵活度更大。所以,了解DragShadowBuilder与OnDragListener肯定是必要的。ViewDragHelper我会在以后的博客中单独讲解。

一个简单的例子

  一个简单的例子,快速了解DragShadowBuilder与OnDragListener如何使用。有一个大概的了解后我们在深入一些细节与一些实际开发中使用的复杂例子

效果图

代码

注意,下面是用RecyclerView作为父类容器,对RecyclerView的子View进行拖拽。所以下面只贴出RecyclerView.Adapter的部分关键代码。 

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val binding = ItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    val viewHolder = ViewHolder(binding)

    binding.root.setOnDragListener { affectedView, event ->
        //注意这个 event.localState其实就是下面的startDragAndDrop方法传入的view
        Log.e("zh", "拖拽开始: 正在被拖拽的view = ${event.localState}")
        /*
            这里回调的affectedView,是event.localState这个被拖拽的子view碰到了其他子view。
            你可以想象我正在拖动A子View到B子View上,affectedView就是这个受到影响的B子view。它提供出来是让你处理A与B之间的交互。
            通常开发例子就是A和B交换位置。
         */
        Log.e("zh", "拖拽开始: 受到影响的其他子view = ${affectedView}")
        Log.e("zh", "拖拽开始: 拖拽 x= ${event.x}")
        Log.e("zh", "拖拽开始: 拖拽 y= ${event.y}")
        when (event.action) {
            /**
             * 拖拽开始
             */
            DragEvent.ACTION_DRAG_STARTED -> {
                return@setOnDragListener true
            }
            /**
             * 进入拖放区域
             */
            DragEvent.ACTION_DRAG_ENTERED -> {
                return@setOnDragListener true
            }

            /**
             * 拖拽位置发生变化
             *
             * 在ACTION_DRAG_ENTERED之后发送给视图,而拖动阴影仍在视图对象的边界框内,但不在可以接受数据的后代视图中。
             * getX()和getY()方法提供了拖动点在View对象的边界框中的X和Y位置。
             */
            DragEvent.ACTION_DRAG_LOCATION -> {
                return@setOnDragListener true
            }
            /**
             * 离开拖放区域
             * 示用户已经将拖动阴影移出视图的边界框,或者移到可以接受数据的后代视图中。视图可以通过改变其外观来做出反应,告诉用户视图不再是直接放置的目标。
             */
            DragEvent.ACTION_DRAG_EXITED -> {

                return@setOnDragListener true
            }
            /**
             * 释放并完成拖拽操作
             */
            DragEvent.ACTION_DROP -> {
                return@setOnDragListener true
            }
            /**
             * 拖拽结束
             */
            DragEvent.ACTION_DRAG_ENDED -> {
                return@setOnDragListener true
            }
        }
        return@setOnDragListener true
    }
    //长按触发拖拽
    binding.root.setOnLongClickListener { view ->
        val shadowBuilder = View.DragShadowBuilder(view)
        /**
         * 开始拖拽
         * 第一个参数为携带数据,这里先不关注,所以设置为null
         */
        view.startDragAndDrop(null, shadowBuilder, view, View.DRAG_FLAG_GLOBAL)
        true
    }
    return viewHolder
}

startDragAndDrop携带的FLAG参数意思

  • DRAG_FLAG_GLOBAL :  拖拽操作是在全局上下文中进行的,不仅限于当前应用程序。例如,您可以拖放来自一个应用程序并将其移到另一个应用程序中。
  • DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION :  拖拽操作包涵了获取一些URI的持久的URIs许可,以使持久的存储进程可以在之后访问这些URI而不需要用户许可。
  • DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION :  拖拽操作包含获取URI的前缀以授予许可,以访问以该前缀开头的URI。
  • DRAG_FLAG_GLOBAL_URI_READ :  拖拽操作包含访问URI的读权限。
  • DRAG_FLAG_GLOBAL_URI_WRITE :  拖拽操作包含访问URI的写权限。
  • DRAG_FLAG_OPAQUE :  拖拽时使用不透明的图像代替被拖拽视图在屏幕上占用的位置。
  • DRAG_FLAG_ACCESSIBILITY_ACTION :  拖拽操作可以激活无障碍操作,例如TalkBack或Switch Access。

实现一个拖拽交换位置的例子

效果图

代码

下面的代码还是RecyclerView.Adapter的部分关键代码。 

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val binding = ItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    val viewHolder = ViewHolder(binding)

    binding.root.setOnDragListener { affectedView, event ->

        when (event.action) {
            /**
             * 拖拽开始
             */
            DragEvent.ACTION_DRAG_STARTED -> {
                if (event.localState == affectedView) {
                    //启动拖拽时将拖拽的View隐藏
                    binding.root.setVisibility(View.INVISIBLE);
                }
                return@setOnDragListener true
            }
            /**
             * 进入拖放区域
             */
            DragEvent.ACTION_DRAG_ENTERED -> {
                return@setOnDragListener true
            }

            /**
             * 拖拽位置发生变化
             */
            DragEvent.ACTION_DRAG_LOCATION -> {
                /*
                 * 因为每一个注册了setOnDragListener的view在有拖动事件重叠的时候都会触发,如果不增加判断直接在ACTION_DRAG_LOCATION里处理数据,会出现有多个回调同时处理的情况
                 * 所以这里需要增加if (event.localState == mCurrentDragView) 判断,将回调处理只锁定在我们拖动的view上
                 */
                if (event.localState == mCurrentDragView) {
                    var dragViewPosition = parent.indexOfChild(event.localState as View)
                    var affectedViewPosition = parent.indexOfChild(affectedView)
                    Log.e("zh", "拖拽位置发生变化:dragViewPosition = ${dragViewPosition}")
                    Log.e("zh", "拖拽位置发生变化:affectedViewPosition = ${affectedViewPosition}")
                    //交换我们数据List的位置
                    Collections.swap(mList, dragViewPosition, affectedViewPosition)
                    //交换Adapter Item的视图位置
                    this.notifyItemMoved(dragViewPosition, affectedViewPosition)
                }
                return@setOnDragListener true
            }
            /**
             * 离开拖放区域
             */
            DragEvent.ACTION_DRAG_EXITED -> {
                return@setOnDragListener true
            }
            /**
             * 释放并完成拖拽操作
             */
            DragEvent.ACTION_DROP -> {
                return@setOnDragListener true
            }
            /**
             * 拖拽结束
             */
            DragEvent.ACTION_DRAG_ENDED -> {
                if (event.localState == affectedView) {
                    //拖拽结束,将拖拽的View重新显示
                    binding.root.setVisibility(View.VISIBLE)
                }
                return@setOnDragListener true
            }
        }
        return@setOnDragListener true
    }
    //长按触发拖拽
    binding.root.setOnLongClickListener { view ->
        mCurrentDragView = view
        val shadowBuilder = View.DragShadowBuilder(view)
        /**
         * 开始拖拽
         */
        view.startDragAndDrop(null, shadowBuilder, view, View.DRAG_FLAG_GLOBAL)
        true
    }
    return viewHolder
}

 

 

end