Compose 中的拖放

1. 开始之前

本 Codelab 提供在 Compose 中实现拖放操作基础知识的实用指导。您将学习如何在应用内和跨不同应用启用视图的拖放功能。您将学习如何在应用内甚至跨不同应用实现拖放操作。

前提条件

要完成本 Codelab,您需要

您将做什么

创建一个简单的应用,实现以下功能

  • 使用 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. 设置

创建一个新项目并选择“空 Activity”模板

19da275afd995463.png

保留所有参数的默认值。

在本 Codelab 中,我们将使用 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")
}

同样创建放置目标图片。

@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. 配置拖动源

让我们为 DragImage 可组合项添加一个拖放源修饰符

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

这里我们添加了一个 dragAndDropSource 修饰符。dragAndDropSource 修饰符为应用它的任何元素启用拖放功能。它将拖动的元素直观地表示为拖动阴影。

dragAndDropSource 修饰符提供了 PointerInputScope 来检测拖动手势。我们使用 detectTapGesture PointerInputScope 来检测长按,即我们的拖动手势。

onLongPress 方法中,我们正在启动正在拖动的数据的传输。

startTransfer 使用 transferData 作为手势完成后要传输的数据来启动拖放会话。它接受封装在 DragAndDropTransferData 中的数据,该数据包含 3 个字段

  1. Clipdata: 要传输的实际数据
  2. flags:控制拖放操作的标志
  3. localState:在同一 Activity 中拖动时会话的本地状态

ClipData 是一个复杂对象,包含不同类型的数据项,包括文本、标记、音频、视频等。在本 Codelab 中,我们使用 imageurl 作为 ClipData 中的一个项。

太好了,现在我们的视图可以拖动了!

415dcef002492e61.gif

5. 配置放置目标

要使视图接受放置的项,它应该添加 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 项并将其分配给图片 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 for Drag and Drop 提供了易于使用的接口,用于在 Compose 中使用视图修饰符实现拖放功能。

总而言之,您已经学会了使用 Compose 实现拖放。请进一步查阅文档。

了解更多