在 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. 设置

创建一个新项目,然后选择“Empty Activity”模板:

19da275afd995463.png

将所有参数保留为默认值。

在本 Codelab 中,我们将使用 ImageView 演示拖放功能。我们来为 Compose 添加 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")
}

在您的可组合项中添加一个列可组合项,以包含这两张图片。

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 来检测我们的拖动手势 longPress。

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:允许可组合项检查启动了会话的 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
)

我们希望当拖动的项进入放置目标区域时,将色调颜色应用于图片。为此,我们可以替换 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 实现拖放功能。请进一步探索相关文档。

了解更多内容