前言
在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