Compose 的拖曳功能

1. 事前準備

本程式碼研究室提供實用操作說明,讓您瞭解實作 Compose 拖曳作業的基礎知識。您將瞭解如何在應用程式內和不同應用程式間拖曳檢視區塊,以及如何在應用程式內及甚至不同應用程式間實作拖曳作業。

必要條件

如要完成本程式碼研究室,您需符合以下條件:

執行步驟

建立簡易應用程式,執行以下動作:

  • 使用 DragAndDropSource 修飾符,將可組合函式設為可拖曳
  • 使用 DragAndDropTarget 修飾符,將可組合函式設為放置目標

軟硬體需求

2. 拖曳事件

拖曳作業可視為 4 階段事件,分別為以下階段:

  1. 啟動:系統為因應使用者的拖曳手勢而啟動拖曳作業。
  2. 繼續:使用者繼續拖曳。
  3. 結束:使用者在放置目標可組合函式中放開拖曳的項目。
  4. 存在:系統傳送信號以結束拖曳作業。

系統會透過 DragEvent 物件傳送拖曳事件。DragEvent 物件可包含下列資料:

  1. ActionType:拖曳事件的生命週期事件所對應的事件動作值,例如 ACTION_DRAG_STARTEDACTION_DROP
  2. ClipData:要拖曳的資料,封裝在 ClipData 物件中
  3. ClipDescription:ClipData 物件相關中繼資訊
  4. Result:拖曳作業的結果
  5. X:被拖曳物件目前位置的 x 座標
  6. Y:被拖曳物件目前位置的 y 座標

3. 設定

建立新專案並選取「Empty Activity」範本:

19da275afd995463.png

請保留所有參數的預設值。

在本程式碼研究室中,我們會使用 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,以垂直方式顯示兩張圖片。您應該可以看到這個畫面。

5e12c26cb2ad1068.png

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 個欄位的資料:

  1. Clipdata::要轉移的實際資料
  2. flags:控制拖曳作業的旗標
  3. localState:在同一活動中拖曳時,工作階段的本機狀態

ClipData 是複雜的物件,內含各種類型的項目,包括文字、標記、音訊、影片等。以本程式碼研究室為例,我們會使用 imageurl 做為 ClipData 中的項目。

很好,現在可以拖曳檢視區塊了!

415dcef002492e61.gif

5. 設定 Drop

如要讓檢視畫面接受放置的項目,應新增 dragAndDropTarget modifier

Modifier.dragAndDropTarget(
   shouldStartDragAndDrop = {
       // condition to accept dragged item
   },
   target = // DragAndDropTarget
   )
)

dragAndDropTarget 修飾符可允許在可組合函式中拖曳資料。這個修飾符有兩個參數:

  1. shouldStartDragAndDrop:允許 Composable 檢查啟動工作階段的 DragAndDropEvent,判斷是否要從指定拖曳工作階段接收資料。
  2. target:即為 DragAndDropTarget,可接收指定拖曳工作階段的事件。

接下來新增要將拖曳事件傳遞至 DragAndDropTarget 的條件。

shouldStartDragAndDrop = { event ->
   event.mimeTypes()
       .contains(ClipDescription.MIMETYPE_TEXT_PLAIN)
}

此處新增的條件,僅在至少一個拖曳項目為純文字時,才接受放置動作。如果所有項目都不是純文字,就不會啟用放置目標。

我們可以為目標參數建立 DragAndDropTarget 物件,處理放置工作階段。

val dndTarget = remember{
   object : DragAndDropTarget{
       // handle Drag event
   }
}

DragAndDropTarget 為拖曳工作階段中的每個階段,提供要覆寫的回呼。

  1. onDrop一個項目已放置在這個 DragAndDropTarget 內,傳回 true 表示已取用 DragAndDropEvent,false 表示已遭拒。
  2. onStarted剛才已啟動拖曳工作階段,且這個 DragAndDropTarget 符合接收資格。這讓您有機會設定 DragAndDropTarget 的狀態,準備取用拖曳工作階段。
  3. onEntered要放置的項目已進入這個 DragAndDropTarget 的邊界。
  4. onMoved要放置的項目已在這個 DragAndDropTarget 的邊界內移動。
  5. onExited要放置的項目已移出這個 DragAndDropTarget 的邊界。
  6. onChanged目前拖曳工作階段中的事件在 DragAndDropTarget 邊界內有所變更,可能已按下或放開輔助鍵。
  7. 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
)

好極了,現在可以順利執行拖曳作業!

277ed56f80460dec.gif

雖然我們新增了拖曳功能,但很難以視覺化方式理解具體情況。我們來調整一下吧!

針對放置目標可組合函式,對圖片套用 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
           )
   )
}

好棒,我們可以為拖曳作業加上視覺提示了!

6be7e749d53d3e7e.gif

6. 恭喜!

Compose for Drag and Drop 提供簡單的介面,方便您使用檢視區塊專用的修飾符,在 Compose 中實作拖曳功能。

總而言之,您已學會如何使用 Compose 實作拖曳功能,歡迎進一步瀏覽說明文件。

瞭解詳情