前言
二维拖拽是允许组件可以在平面上被自由拖动,不限制它的拖动方向。
可是 Compose 并没有直接提供监测二维拖动的 Modifier,只提供了监测一维拖动(水平或垂直)的 Modifier.draggable
,那怎么办呢?
我们可以使用更底层的 Modifier.pointerInput
配合上 detectDragGestures
来实现二维拖动的监测,我们先来看看 Modifier.pointerInput
。
配合实现二维拖动
Modifier.pointerInput:底层指针事件的入口
Modifier.pointerInput 之前有提到过,你可以看我的这篇文章:传送门,所以我们就简要说说。
Modifier.pointerInput
是 Jetpack Compose 中处理所有的最底层指针事件的入口,它的函数原型:
fun Modifier.pointerInput(
key1: Any?,
block: PointerInputEventHandler // 手势识别逻辑 lambda 表达式
): Modifier
当 key
参数改变时,pointerInput
就会重启,内部运行手势检测逻辑的协程会被取消,我们可以用它来改变内部的手势识别逻辑。
参数 block
提供了一个协程作用域,我们就是在这里进行手势的识别逻辑。并且作用域具有 PointerInputScope
环境,我们可以访问组件的尺寸、视图配置(如触摸阈值),并且使用一些非常方便的函数去识别手势,例如:
@Composable
fun PointerInputSample() {
Box(
Modifier
.background(Color.Green)
.size(100.dp)
.pointerInput(key1 = Unit) {
detectTapGestures(
onPress = { offset ->
println("你按压了绿色方块,按压位置:$offset")
},
onLongPress = { offset ->
println("你长按了绿色方块,长按位置:$offset")
},
onDoubleTap = { offset ->
println("你双击了绿色方块,双击位置:$offset")
},
onTap = { offset ->
println("你点击了绿色方块,点击位置:$offset")
}
)
}
)
}
detectDragGestures
detectDragGestures
可以从原始的指针事件流中识别出二维拖动手势,它的函数原型:
@OptIn(ExperimentalFoundationApi::class)
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = {}, // 手指按下并开始拖动(超过触摸阈值 touch slop)时的回调
onDragEnd: () -> Unit = {}, // 拖动正常结束时的回调
onDragCancel: () -> Unit = {}, // 拖动被打断的回调
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit // 手指每次拖动的回调(正在拖动)
)
先来看最核心的 onDrag
回调:
Modifier.pointerInput(key1 = Unit) {
detectDragGestures { change, dragAmount ->
}
}
dragAmount
参数是相对上一次回调发生时,手指的移动距离。
change
参数封装了当前的指针事件中,用于拖动手势的触摸点的详细信息,比如有触摸点的id(多点触摸下,有多个指针)、事件发生的时间、当前触摸的位置、上一次触摸的位置。
实际上dragAmount
参数是一个便捷参数,它的值是通过当前触摸的位置减去上一次触摸的位置计算得出的。
另外拖动开始的回调中,startPosition
参数是拖动开始的位置在组件内的坐标。
可拖动的文字组件
了解了这些,来写一个文字可以在屏幕上自由拖动的示例:
@Composable
fun DraggableTextSample() {
var offsetX by remember { mutableFloatStateOf(0f) }
var offsetY by remember { mutableFloatStateOf(0f) }
var isDragging by remember { mutableStateOf(false) } // 是否正在拖动
Box(
Modifier.fillMaxSize()
) {
Text(
text = if (isDragging) "我正在被拖动..." else "请随意拖动我!",
modifier = Modifier
.align(Alignment.Center) // 初始居中,偏移是相对于这个中心位置
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(if (isDragging) Color.Cyan.copy(alpha = 0.7f) else Color.LightGray) // 拖动时改变背景色
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { startOffset ->
isDragging = true
Log.d(
"DraggableTextSample",
"拖动开始,初始触摸位置 (相对于组件左上角): $startOffset, 当前偏移: ($offsetX, $offsetY)"
)
},
onDragEnd = {
isDragging = false
Log.d("DraggableTextSample", "拖动结束,最终偏移: ($offsetX, $offsetY)")
},
onDragCancel = {
isDragging = false
Log.d("DraggableTextSample", "拖动被取消")
},
onDrag = { change, dragAmount ->
change.consume() // 消费掉位置变化,防止其他父组件响应此拖动
// 累积拖动带来的偏移量
offsetX += dragAmount.x
offsetY += dragAmount.y
}
)
}
.padding(12.dp)
)
}
}
运行效果:
注意:
如果不在 onDrag
回调中调用 change.consume()
消费掉传递的指针事件,会将指针事件传递给可滚动的父组件,导致拖动文字组件会触发父组件的滚动行为。
最后与 detectDragGestures
类似的函数还有三个:detectDragGesturesAfterLongPress
、detectHorizontalDragGestures
、detectVerticalDragGestures
。
后两个对应一维的拖动监测,第一个用来做长按之后的拖动监测。
detectDragGestures 与 Modifier.draggable 对比
我们知道 Modifier.draggable
是用于一维拖动的,那它和我们现在讲的 detectDragGestures
有什么区别吗?
实际没有本质上的区别,但 Modifier.draggable
抽象层级更高,它内置了状态管理 (DraggableState
),封装了拖动状态和一些内部控制方法,比如你可以使用代码控制拖动的行为:
@Composable
fun DraggableStateChange() {
var offsetX by remember { mutableFloatStateOf(0f) }
val draggableState = rememberDraggableState { delta ->
offsetX += delta
}
Box(Modifier.fillMaxSize()) {
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.size(100.dp)
.background(Green)
.draggable(state = draggableState, orientation = Orientation.Horizontal)
)
}
LaunchedEffect(key1 = Unit) {
delay(2000)
draggableState.drag {
// 向右拖动 100 像素
dragBy(200f)
}
delay(1000)
draggableState.drag {
// 向左拖动 100 像素
dragBy(-200f)
}
}
}
运行结果:
而 detectDragGestures
你要自己管理状态并实现相应逻辑。
使用原则:
如果要实现二维平面上的拖动,就必须使用 detectDragGestures
,否则就那个满足需求用哪个,Modifier.draggable
通常更简单。