Compose でのドラッグ&ドロップ

1. 始める前に

この Codelab では、Compose のドラッグ&ドロップ オペレーションを実装するための基本的な手順について説明します。アプリ内とアプリ間の両方で、ビューのドラッグ&ドロップを有効にするとともに、ドラッグ&ドロップ オペレーションを実装する方法を学びます。

前提条件

この Codelab を完了するには、以下が必要です。

演習内容

次の機能がある簡単なアプリを作成します。

  • dragAndDropSource 修飾子を使用してコンポーザブルをドラッグ可能に設定する
  • dragAndDropTarget 修飾子を使用してコンポーザブルをドロップ ターゲットとして設定する
  • Compose を使用してリッチ コンテンツを受信する

必要なもの

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

すべてのパラメータをデフォルトのままにします。

この Codelab では、ImageView を使用してドラッグ&ドロップ機能を実演します。プロジェクトをコンポーズして同期するために、Glide ライブラリの Gradle 依存関係を追加しましょう。

implementation("com.github.bumptech.glide:compose:1.0.0-beta01")

MainActivity.kt で画像の composable を作成します。これはドラッグソースとして機能します。

@Composable
fun DragImage(url: String) {
   GlideImage(model = url, contentDescription = "Dragged Image")
}

同様に、ドロップ ターゲットの画像を作成します。

@Composable
fun DropTargetImage(url: String) {
   val urlState = remember {mutableStateOf(url)}
   GlideImage(model = urlState.value, contentDescription = "Dropped Image")
}

列コンポーザブルをコンポーザブルに追加して、この 2 つの画像を含めます。

Column {
   DragImage(url = getString(R.string.source_url))
   DropTargetImage(url = getString(R.string.target_url))
}

この段階では、MainActivity に 2 つの画像が縦方向に表示されます。画面は次のようになります。

5e12c26cb2ad1068.png

4. ドラッグソースの設定

DragImage コンポーザブルにドラッグ&ドロップ ソースの修飾子を追加しましょう。

modifier = Modifier.dragAndDropSource {
   detectTapGestures(
       onLongPress = {
           startTransfer(
               DragAndDropTransferData(
                   ClipData.newPlainText("image uri", url)
               )
           )
       }
   )
}

ここでは、dragAndDropSource 修飾子を追加しています。dragAndDropSource 修飾子は、適用先の要素のドラッグ&ドロップ機能を有効にするとともに、ドラッグされた要素をドラッグ シャドウとして視覚的に表現します。

dragAndDropSource 修飾子は、ドラッグ操作を検出する PointerInputScope を提供します。ここでは、ドラッグ操作である長押しを検出する detectTapGesture PointerInputScope を使用しています。

onLongPress メソッドで、ドラッグ対象データの転送を開始します。

startTransfer は、操作の完了時に転送するデータとして transferData を指定して、ドラッグ&ドロップ セッションを開始します。その際、次の 3 つのフィールドを持つ DragAndDropTransferData にデータをカプセル化します。

  1. Clipdata: 転送する実際のデータ
  2. flags: ドラッグ&ドロップ オペレーションを制御するフラグ
  3. localState: 同じアクティビティでドラッグしたときのセッションのローカル状態

ClipData は、テキスト、マークアップ、音声、動画など、さまざまな種類のアイテムを含む複合オブジェクトです。この Codelab では、ClipData のアイテムとして imageurl を使用しています。

これで、ビューをドラッグできるようになりました。

415dcef002492e61.gif

5. ドロップの設定

ドロップされたアイテムをビューで受け入れるようにするには、dragAndDropTarget modifier を追加する必要があります。

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

dragAndDropTarget は、コンポーザブルでデータをドラッグできるようにする修飾子です。この修飾子には 2 つのパラメータがあります。

  1. shouldStartDragAndDrop: セッションを開始した DragAndDropEvent を調べることで、コンポーザブルが特定のドラッグ&ドロップ セッションからイベントを受け取るかどうかを決定できるようにします。
  2. target: 特定のドラッグ&ドロップ セッションのイベントを受け取る DragAndDropTarget です。

ドラッグ イベントを DragAndDropTarget に渡す条件を追加しましょう。

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

ここで追加した条件は、ドラッグされるアイテムの少なくとも 1 つが書式なしテキストである場合にのみ、ドロップ アクションを許可するというものです。どのアイテムも書式なしテキストでない場合、ドロップ ターゲットは有効になりません。

ターゲット パラメータとして、ドロップ セッションを処理する DragAndDropTarget のオブジェクトを作成しましょう。

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

DragAndDropTarget には、ドラッグ&ドロップ セッションの各ステージでオーバーライドされるコールバックがあります。

  1. onDrop : アイテムがこの DragAndDropTarget 内にドロップされました。DragAndDropEvent が使用されたことを示す場合は true を、拒否されたことを示す場合は 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 アイテムを抽出して画像の URL に割り当て、ドロップが正しく処理されたことを示すために 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
)

ドラッグされたアイテムがドロップ ターゲット領域に入ったときに、画像に色合いを適用します。 そのためには、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 は、ビューの修飾子を使用して Compose でドラッグ&ドロップ機能を実装するための簡単なインターフェースを提供します。

この Codelab では、Compose を使用してドラッグ&ドロップを実装する方法を学びました。詳しくは、以下のドキュメントをご覧ください。

詳細