Compose 的拖曳功能

1. 事前準備

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

必要條件

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

執行步驟

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

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

軟硬體需求

2. 拖曳事件

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

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

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

  1. ActionType:拖曳事件的生命週期事件所對應的事件動作值,例如 ACTION_DRAG_STARTED、ACTION_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 實作拖曳功能,歡迎進一步瀏覽說明文件。

瞭解詳情