1. 事前準備
本程式碼研究室提供實用操作說明,讓您瞭解實作 Compose 拖曳作業的基礎知識。您將瞭解如何在應用程式內和不同應用程式間拖曳檢視區塊,以及如何在應用程式內及甚至不同應用程式間實作拖曳作業。
必要條件
如要完成本程式碼研究室,您需符合以下條件:
- 具備建構 Android 應用程式的經驗
- 具備使用 Jetpack Compose 和修飾符的經驗
執行步驟
建立簡易應用程式,執行以下動作:
- 使用 DragAndDropSource 修飾符,將可組合函式設為可拖曳
- 使用 DragAndDropTarget 修飾符,將可組合函式設為放置目標
軟硬體需求
- Android Studio Jellyfish 以上版本
- Android 裝置或模擬器
2. 拖曳事件
拖曳作業可視為 4 階段事件,分別為以下階段:
- 啟動:系統為因應使用者的拖曳手勢而啟動拖曳作業。
- 繼續:使用者繼續拖曳。
- 結束:使用者在放置目標可組合函式中放開拖曳的項目。
- 存在:系統傳送信號以結束拖曳作業。
系統會透過 DragEvent 物件傳送拖曳事件。DragEvent 物件可包含下列資料:
- ActionType:拖曳事件的生命週期事件所對應的事件動作值,例如- ACTION_DRAG_STARTED、- ACTION_DROP等
- ClipData:要拖曳的資料,封裝在 ClipData 物件中
- ClipDescription:ClipData 物件相關中繼資訊
- Result:拖曳作業的結果
- X:被拖曳物件目前位置的 x 座標
- Y:被拖曳物件目前位置的 y 座標
3. 設定
建立新專案並選取「Empty Activity」範本:

請保留所有參數的預設值。
在本程式碼研究室中,我們會使用 ImageView 來示範拖曳功能。請為 Compose 的 Glide 程式庫新增 Gradle 依附元件,並同步處理專案。
implementation("com.github.bumptech.glide:compose:1.0.0-beta01")
現在在 MainActivity.kt 中,為 Image 建立 composable,做為拖曳來源:
@Composable
fun DragImage(url: String) {
   GlideImage(model = url, contentDescription = "Dragged Image")
}
同樣地,建立 Drop 目標圖片。
@Composable
fun DropTargetImage(url: String) {
   val urlState = remember {mutableStateOf(url)}
   GlideImage(model = urlState.value, contentDescription = "Dropped Image")
}
在可組合函式中新增 Column 可組合函式,納入這兩張圖片。
Column {
   DragImage(url = getString(R.string.source_url))
   DropTargetImage(url = getString(R.string.target_url))
}
在這個階段,我們已設定 MainActivity,以垂直方式顯示兩張圖片。您應該可以看到這個畫面。

4. 設定 Drag 來源
我們現在要為 DragImage 可組合函式加入拖曳來源的修飾符:
modifier = Modifier.dragAndDropSource {
   detectTapGestures(
       onLongPress = {
           startTransfer(
               DragAndDropTransferData(
                   ClipData.newPlainText("image uri", url)
               )
           )
       }
   )
}
我們在此加入了 dragAndDropSource 修飾符。dragAndDropSource 修飾符可對套用的任何元素啟用拖曳功能,並透過視覺化方式,以拖曳陰影呈現被拖曳的元素。
dragAndDropSource 修飾符可提供 PointerInputScope 來偵測拖曳手勢。我們已使用 detectTapGesture PointerInputScope 偵測 longPress (亦即拖曳手勢)。
onLongPress 方法會啟動被拖曳資料的轉移程序。
startTransfer 會啟動拖曳工作階段,並在手勢完成時,使用 TransferData 做為要轉移的資料。這項程序會採用封裝在 DragAndDropTransferData 中,具有以下 3 個欄位的資料:
- Clipdata::要轉移的實際資料
- flags:控制拖曳作業的旗標
- localState:在同一活動中拖曳時,工作階段的本機狀態
ClipData 是複雜的物件,內含各種類型的項目,包括文字、標記、音訊、影片等。以本程式碼研究室為例,我們會使用 imageurl 做為 ClipData 中的項目。
很好,現在可以拖曳檢視區塊了!

5. 設定 Drop
如要讓檢視畫面接受放置的項目,應新增 dragAndDropTarget modifier:
Modifier.dragAndDropTarget(
   shouldStartDragAndDrop = {
       // condition to accept dragged item
   },
   target = // DragAndDropTarget
   )
)
dragAndDropTarget 修飾符可允許在可組合函式中拖曳資料。這個修飾符有兩個參數:
- shouldStartDragAndDrop:允許 Composable 檢查啟動工作階段的 DragAndDropEvent,判斷是否要從指定拖曳工作階段接收資料。
- target:即為 DragAndDropTarget,可接收指定拖曳工作階段的事件。
接下來新增要將拖曳事件傳遞至 DragAndDropTarget 的條件。
shouldStartDragAndDrop = { event ->
   event.mimeTypes()
       .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
}
此處新增的條件,僅在至少一個拖曳項目為純文字時,才接受放置動作。如果所有項目都不是純文字,就不會啟用放置目標。
我們可以為目標參數建立 DragAndDropTarget 物件,處理放置工作階段。
val dndTarget = remember{
   object : DragAndDropTarget{
       // handle Drag event
   }
}
DragAndDropTarget 為拖曳工作階段中的每個階段,提供要覆寫的回呼。
- onDrop:一個項目已放置在這個 DragAndDropTarget 內,傳回 true 表示已取用 DragAndDropEvent,false 表示已遭拒。
- onStarted:剛才已啟動拖曳工作階段,且這個 DragAndDropTarget 符合接收資格。這讓您有機會設定 DragAndDropTarget 的狀態,準備取用拖曳工作階段。
- onEntered:要放置的項目已進入這個 DragAndDropTarget 的邊界。
- onMoved:要放置的項目已在這個 DragAndDropTarget 的邊界內移動。
- onExited:要放置的項目已移出這個 DragAndDropTarget 的邊界。
- onChanged:目前拖曳工作階段中的事件在 DragAndDropTarget 邊界內有所變更,可能已按下或放開輔助鍵。
- onEnded:拖曳工作階段已完成。在先前收到 onStarted 事件的階層中,所有 DragAndDropTarget 例項都會收到這個事件。這時可以重設 DragAndDropTarget 的狀態。
接下來,定義當項目放到目標可組合函式中的處理方式。
override fun onDrop(event: DragAndDropEvent): Boolean {
   val draggedData = event.toAndroidDragEvent().clipData.getItemAt(0).text
   urlState.value = draggedData.toString()
   return true
}
在 onDrop 函式中,我們會擷取 ClipData 項目並指派給圖片網址,同時傳回 true,表示已正確處理放置作業。
我們不必將這個 DragAndDropTarget 例項指派給 dragAndDropTarget 修飾符的目標參數:
Modifier.dragAndDropTarget(
   shouldStartDragAndDrop = { event ->
       event.mimeTypes()
           .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
   },
   target = dndTarget
)
好極了,現在可以順利執行拖曳作業!

雖然我們新增了拖曳功能,但很難以視覺化方式理解具體情況。我們來調整一下吧!
針對放置目標可組合函式,對圖片套用 ColorFilter:
var tintColor by remember {
   mutableStateOf(Color(0xffE5E4E2))
}
定義色調顏色後,在圖片中加入 ColorFilter:
GlideImage(
   colorFilter = ColorFilter.tint(color = backgroundColor,
       blendMode = BlendMode.Modulate),
   // other params
)
我們希望在拖曳項目進入 Drop 目標區域時,將色調顏色套用至圖片。我們可以藉由覆寫 onEntered 回呼達到這個效果。
override fun onEntered(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xff00ff00)
}
此外,當使用者拖曳到目標區域以外時,畫面應該回復為原本的色彩濾鏡。為此,我們必須覆寫 onExited 回呼:
override fun onExited(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xffE5E4E2)
}
順利完成拖曳程序後,也可以還原為原本的 ColorFilter:
override fun onEnded(event: DragAndDropEvent) {
   super.onEntered(event)
   tintColor = Color(0xffE5E4E2)
}
最後,放置可組合函式大致如下:
@Composable
fun DropTargetImage(url: String) {
   val urlState = remember {
       mutableStateOf(url)
   }
   var tintColor by remember {
       mutableStateOf(Color(0xffE5E4E2))
   }
   val dndTarget = remember {
       object : DragAndDropTarget {
           override fun onDrop(event: DragAndDropEvent): Boolean {
               val draggedData = event.toAndroidDragEvent()
                   .clipData.getItemAt(0).text
               urlState.value = draggedData.toString()
               return true
           }
           override fun onEntered(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xff00ff00)
           }
           override fun onEnded(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xffE5E4E2)
           }
           override fun onExited(event: DragAndDropEvent) {
               super.onEntered(event)
               tintColor = Color(0xffE5E4E2)
           }
       }
   }
   GlideImage(
       model = urlState.value,
       contentDescription = "Dropped Image",
       colorFilter = ColorFilter.tint(color = tintColor,
           blendMode = BlendMode.Modulate),
       modifier = Modifier
           .dragAndDropTarget(
               shouldStartDragAndDrop = { event ->
                   event
                       .mimeTypes()
                       .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
               },
               target = dndTarget
           )
   )
}
好棒,我們可以為拖曳作業加上視覺提示了!

6. 恭喜!
Compose for Drag and Drop 提供簡單的介面,方便您使用檢視區塊專用的修飾符,在 Compose 中實作拖曳功能。
總而言之,您已學會如何使用 Compose 實作拖曳功能,歡迎進一步瀏覽說明文件。
