在现代桌面应用中,拖拽(Drag-and-Drop)操作是一种极其直观且提升用户体验的交互方式。它允许用户通过直接操作界面元素来移动数据、重新组织内容,从而极大地简化了复杂任务。如果你正在使用 Kotlin Compose Multiplatform 开发桌面应用,并希望为你的应用增添这一强大的交互功能,那么你来对地方了!
本文将深入探讨如何在 Compose Multiplatform Desktop 应用中优雅地实现拖拽功能,我们将从核心概念开始,逐步讲解如何创建可拖拽的源和可接收拖拽的目标,并结合你的实际项目源码,展示高级用法和最佳实践。
Compose Multiplatform 拖拽概述
Compose Multiplatform 提供了一套直观的修饰符(modifiers)来支持拖拽操作。核心是两个修饰符:dragAndDropSource
和 dragAndDropTarget
。
通过这两个修饰符,你可以让你的 Compose Multiplatform 应用能够:
- 接收用户从其他应用程序拖入的数据。
- 允许用户将数据从你的应用中拖出。
接下来,让我们分别看看如何创建拖拽源和拖拽目标。
创建拖拽源(Drag Source)
kotlin
fun Modifier.dragAndDropSource(
drawDragDecoration: DrawScope.() -> Unit,
transferData: (Offset) -> DragAndDropTransferData?
): Modifier =
this then
DragAndDropSourceElement(
drawDragDecoration = drawDragDecoration,
// TODO: Expose this as public argument
detectDragStart = DragAndDropSourceDefaults.DefaultStartDetector,
transferData = transferData
)
在你的 Compose 组件中,你可以使用 dragAndDropSource
修饰符来定义一个拖拽源。这个修饰符需要两个参数:
drawDragDecoration
: 一个绘制函数,用于在拖拽时显示拖拽组件。transferData
: 一个数据传输函数,用于在拖拽开始时提供要传输的数据。
接下来以我开源项目 CrossPaste 为例,展示如何实现优雅的拖拽源:
SidePastePreviewItemView.kt 完整实现点这里
kotlin
// 我们使用 graphicsLayer 记录拖拽源渲染的内容
val graphicsLayer = rememberGraphicsLayer()
Row(
modifier =
Modifier
.dragAndDropSource(
drawDragDecoration = {
// 使用 graphicsLayer 绘制拖拽装饰
runBlocking {
runCatching {
graphicsLayer.toImageBitmap()
}.getOrNull()
}?.let { bitmap ->
drawImage(
image = bitmap,
topLeft = Offset.Zero,
alpha = 0.9f, // 设置拖拽部分的透明度
)
}
},
) { offset ->
DragAndDropTransferData(
transferable =
DragAndDropTransferable(
pasteProducer
.produce(
pasteData = pasteData,
localOnly = true,
primary = configManager.getCurrentConfig().pastePrimaryTypeOnly,
)?.let {
it as DesktopWriteTransferable
} ?: DesktopWriteTransferable(LinkedHashMap()),
),
supportedActions =
listOf(
DragAndDropTransferAction.Copy,
),
dragDecorationOffset = offset,
onTransferCompleted = { action ->
},
)
}
...
) {
Box(
modifier =
Modifier
.fillMaxSize()
.drawWithContent {
graphicsLayer.record {
this@drawWithContent.drawContent()
}
drawLayer(graphicsLayer)
},
) {
// 这里是你的拖拽源内容
...
}
}
我使用 rememberGraphicsLayer()
来记录拖拽源渲染的内容,并在 dragAndDropSource
中导出为 bitmap 来绘制。这样,当用户开始拖拽时,可以看到一个半透明的拖拽内容,并且这个拖拽内容与拖拽原是一模一样即时渲染的,这在交互时非常重要,可以帮助用户确认拖拽内容正确,没有拖错。
数据传输函数 transferData
需要用户提供一个 DragAndDropTransferData` 对象
kotlin
@OptIn(ExperimentalComposeUiApi::class)
actual class DragAndDropTransferData(
/**
* The object being transferred during a drag-and-drop gesture.
*/
@property:ExperimentalComposeUiApi
val transferable: DragAndDropTransferable,
/**
* The transfer actions supported by the source of the drag-and-drop session.
*/
@property:ExperimentalComposeUiApi
val supportedActions: Iterable<DragAndDropTransferAction>,
/**
* The offset of the pointer relative to the drag decoration.
*/
@property:ExperimentalComposeUiApi
val dragDecorationOffset: Offset = Offset.Zero,
/**
* Invoked when the drag-and-drop gesture completes.
*
* The argument to the callback specifies the transfer action with which the gesture completed,
* or `null` if the gesture did not complete successfully.
*/
@property:ExperimentalComposeUiApi
val onTransferCompleted: ((userAction: DragAndDropTransferAction?) -> Unit)? = null,
)
DragAndDropTransferable
在 Desktop 环境实际上就是awt
的Transferable
,它包含了要传输的数据。(对于不熟悉Transferable
的同学,可以简单理解为它是一个数据容器,它返回其支持的所有DataFlavor
,每个DataFlavor
可以理解为一种 mime 类型,使用getTransferData
方法传入DataFlavor
可以获取对应类型的数据。)supportedActions
是一个可迭代的DragAndDropTransferAction
列表,表示支持的拖拽操作类型(Copy
、Move
、Link
)。dragDecorationOffset
是拖拽装饰的偏移量,通常用于调整拖拽时显示的位置。onTransferCompleted
是一个回调函数,当拖拽操作完成时调用,可以用于处理拖拽结果。

创建拖拽目标(Drop Target)
在你的 Compose 组件中,你可以使用 dragAndDropTarget
修饰符来定义一个拖拽目标。这个修饰符需要一个 DragAndDropTarget
接口的实现,它包含了处理拖拽事件的方法。
kotlin
interface DragAndDropTarget {
fun onDrop(event: DragAndDropEvent): Boolean
fun onStarted(event: DragAndDropEvent) = Unit
fun onEntered(event: DragAndDropEvent) = Unit
fun onMoved(event: DragAndDropEvent) = Unit
fun onExited(event: DragAndDropEvent) = Unit
fun onChanged(event: DragAndDropEvent) = Unit
fun onEnded(event: DragAndDropEvent) = Unit
}
在一般情况下我们只需要关注三个方法:
onStarted(event: DragAndDropEvent)
: 当拖拽操作开始时调用,可以用于更新 UI 状态或准备拖拽目标。onDrop(event: DragAndDropEvent)
: 当拖拽操作完成并释放时调用,处理实际的数据传输。onEnded(event: DragAndDropEvent)
: 当拖拽操作结束时调用,可以用于清理状态或重置 UI。
下面是 CrossPaste 中接受用户拖拽数据记录在粘贴板的实现:
DragTargetContentView.kt 完整实现点这里
kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DragTargetContentView() {
val appWindowManager = koinInject<DesktopAppWindowManager>()
val copywriter = koinInject<GlobalCopywriter>()
val pasteConsumer = koinInject<TransferableConsumer>()
var isDragging by remember { mutableStateOf(false) }
val animatedAlpha by animateFloatAsState(
targetValue = if (isDragging) 0.8f else 0f,
animationSpec = tween(300),
label = "drag_target_alpha",
)
val dragAndDropTarget =
remember {
object : DragAndDropTarget {
override fun onStarted(event: DragAndDropEvent) {
isDragging = true
}
override fun onEnded(event: DragAndDropEvent) {
isDragging = false
}
override fun onDrop(event: DragAndDropEvent): Boolean {
val transferable = event.awtTransferable
val source: String? = appWindowManager.getCurrentActiveAppName()
val pasteTransferable = DesktopReadTransferable(transferable)
return runBlocking {
pasteConsumer.consume(pasteTransferable, source, false)
}.isSuccess
}
}
}
Box(
modifier =
Modifier
.fillMaxSize()
.dragAndDropTarget(
shouldStartDragAndDrop = { true },
target = dragAndDropTarget,
),
) {
... // 这里实现具体的 UI 与动画效果
}
}
-
在这个实现中,通过
onStarted
和onEnded
方法来控制拖拽状态的变化。这方便我们在自己的 UI 组件上层创建蒙版层,基于isDragging
是否显示来控制拖拽时的视觉效果。 -
onDrop
在桌面环境中我们可以从event
中提取awtTransferable
,这与拖拽源中提供的数据结构一样,基于你应用关注的数据类型,你可以在这里进行相应的处理,在 CrossPaste 中我将尝试遍历所有支持的粘贴板类型,将说有数据记录下来,以便用户后续使用。

了解详情
CrossPaste 是一款 跨设备的通用粘贴板,在任意设备间复制粘贴,就像在同一台设备上操作一样自然流畅