观心静

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言

  在Android里实现View的拖拽无需自己去重写OnTouchListener,Android已经提供了DragShadowBuilder与OnDragListener来轻松的实现此类需求。DragShadowBuilder的原理其实复制了一个独立于当前app进程的一个图像进行拖拽。DragShadowBuilder 能在App内正常实现拖拽功能或者跨Activity、Fragment的实现携带数据拖拽效果。还能进行跨进程的拖拽复制内容(DragShadowBuilder是可以携带属性拖拽的),将一个app的文本拖到另一个app里。

区分DragShadowBuilder与ViewDragHelper的区别

  请别将ViewDragHelper与DragShadowBuilder 混在一起理解,很多初学者会将两者的关系搞混,但是二者完全不同。

  ViewDragHelper类也能更简单方便的帮我们实现拖放滑动功能,但是ViewDragHelper需要自定义ViewGroup实现,并且只是针对ViewGroup里的子View进行拖放,在拖放的过程中不能携带数据。也不能跨进程,甚至不能跨activity。所以ViewDragHelper本质上更像是一个ViewGroup里简单实现拖放效果的帮助类。在功能上没有DragShadowBuilder这么强大与灵活。 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。

携带数据拖拽

下面是一个输入文本框内容的拖拽复制例子。

效果图

代码

xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/topLayout"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:layout_marginHorizontal="50dp"
        android:layout_marginTop="20dp"
        android:background="@drawable/shape_text_frame"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <EditText
            android:id="@+id/topEditText"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@null"
            android:padding="20dp"
            android:text="测试文本"
            android:textSize="18sp" />

    </FrameLayout>

    <FrameLayout
        android:id="@+id/bottomLayout"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:layout_marginHorizontal="50dp"
        android:layout_marginTop="20dp"
        android:background="@drawable/shape_text_frame"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/topLayout">

        <EditText
            android:id="@+id/bottomEditText"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@null"
            android:padding="20dp"
            android:text=""
            android:textSize="18sp" />
    </FrameLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

activity


class MainActivity : AppCompatActivity() {
    private val mBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        initView()
    }

    private fun initView() {
        mBinding.topEditText.setOnDragListener { v, event ->
            when (event.action) {
                /**
                 * 释放并完成拖拽操作
                 */
                DragEvent.ACTION_DROP -> {
                    val textContent = event.clipData.getItemAt(0).text
                    mBinding.topEditText.setText(textContent)
                    return@setOnDragListener true
                }
            }
            return@setOnDragListener true
        }
        mBinding.bottomEditText.setOnDragListener { v, event ->
            when (event.action) {
                /**
                 * 释放并完成拖拽操作
                 */
                DragEvent.ACTION_DROP -> {
                    val textContent = event.clipData.getItemAt(0).text
                    mBinding.bottomEditText.setText(textContent)
                    return@setOnDragListener true
                }
            }
            return@setOnDragListener true
        }
        mBinding.topEditText.setOnLongClickListener { view ->
            val shadowBuilder = View.DragShadowBuilder(view)
            val dragData = ClipData.newPlainText("text_content", mBinding.topEditText.text.toString())
            view.startDragAndDrop(dragData, shadowBuilder, view, View.DRAG_FLAG_GLOBAL)
            return@setOnLongClickListener true
        }
        mBinding.bottomEditText.setOnLongClickListener { view ->
            val shadowBuilder = View.DragShadowBuilder(view)
            val dragData = ClipData.newPlainText("text_content", mBinding.bottomEditText.text.toString())
            view.startDragAndDrop(dragData, shadowBuilder, view, View.DRAG_FLAG_GLOBAL)
            return@setOnLongClickListener true
        }
    }
}

里的复制传递数据其实是使用ClipData,而它还可以广泛的应用在跨进程的数据复制中,这点需要额外了解,这里不做赘述。 此外,你还要知道ClipData不单单是可以复制传递文本数据。 它还可以传递图片、html、url、intent等等数据

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

除了用DragShadowBuilder实现拖拽交换位置以外,你还可以参考这篇博客更简单的实现拖拽交换位置  Android开发 RecyclerView实现拖动与滑动ItemTouchHelper

效果图

代码

下面的代码还是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

posted on 2023-06-06 15:16  观心静  阅读(804)  评论(0编辑  收藏  举报