I have a cursor icon that I'd like to keep centered on the user's finger as they drag it, but due to touch slop and a variety of other things, it doesn't stay centered by simply updating the view's offset via the drag amount in onDrag.
I thought I could compute the global X Y of the touch point and just set it to that but it's oddly difficult to get that value in Compose without doing discouraged things like using the pointerInterop modifier.
onDrag = { change, dragAmount ->
change.consume()
params.x += dragAmount.x.toInt()
params.y += dragAmount.y.toInt()
wm.updateViewLayout(composeView, params)
}
UPDATE 1
There's gotta be some obvious math I'm just not seeing. I'm able to sync the view's centerpoint with the user's finger on DragStart by shifting the view by the offset like here:
onDragStart = {
params.x = params.x + it.x.toInt() - (composeView.width / 2)
params.y = params.y + it.y.toInt() - (composeView.height / 2)
wm.updateViewLayout(composeView, params)
},
onDrag = { change, dragAmount ->
change.consume()
//cant do that here though so doing the normal way below
// params.x = params.x + change.position.x.toInt() - (composeView.width / 2)
// params.y = params.y + change.position.y.toInt() - (composeView.height / 2)
params.x += dragAmount.x.toInt()
params.y += dragAmount.y.toInt()
wm.updateViewLayout(composeView, params)
}
If I comment out the dragamount incremental way and use the adjusted way, it goes erratic when you drag it, jumping around crazy.
I tried some other techniques like using awaitEachGesture
(seen below) with drag
to get rid of the touch slop detection but that doesn't get me any closer.
The real problem seems to occur when the user drags VERY slow. If you drag very slow, for some reason your finger is allowed to creep away from the view. It's like the view doesn't respond to very tiny movements.
All I want to do is keep the view centered on the touch point. It seems like it should just be some formula with existing available params..
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
drag(down.id) { change ->
val posChange = change.positionChange()
val pos = change.position
Log.i("await gesture drag", "change: ${pos}")
Log.i("await gesture drag", "poschange: ${posChange}")
change.consume()
params.x += posChange.x.toInt()
params.y += posChange.y.toInt()
// params.x = params.x + pos.x.toInt() - (composeView.width / 2)
// params.y = params.y + pos.y.toInt() - (composeView.height / 2)
wm.updateViewLayout(composeView, params)
}
}
}
I also created this GIF to show the issues. First you can see the touch point has some drift even if I go fast. It goes to the edge of the circle. Then worse, if I go really slow, it drifts a lot.
Update 2
I just realized it might be because the drag amount has to be rounded to an int for the layout params x and y. I'll investigate that tomorrow.
Update 3
My theory about rounding was correct. When you drag very slowly, the change position/drag amount float .toInt()
was rounding down to zero and causing no movement. Changing it to roundToInt
also doesnt work because then slow movements slowly crawl past your finger as it rounds up. The solution for me was to use a float accumulator so that the small drag amounts aren't swallowed. I'm guessing this is because I'm moving around a parent ComposeView rather than from the touched element's own .offset
modifier?
I'm not really sure what the difference is because I know you don't need an accumulator for the usual drag technique, maybe theres a hidden accumulator under the hood?
.pointerInput(Unit) {
val center = size.center
awaitEachGesture {
val down = awaitFirstDown()
params.x += (down.position.x - center.x).roundToInt()
params.y += (down.position.y - center.y).roundToInt()
wm.updateViewLayout(composeView, params)
drag(down.id) { change ->
val posChange = change.positionChange()
change.consume()
accumulatedX += posChange.x
accumulatedY += posChange.y
val deltaX = accumulatedX.toInt()
val deltaY = accumulatedY.toInt()
accumulatedX -= deltaX
accumulatedY -= deltaY
params.x += deltaX
params.y += deltaY
wm.updateViewLayout(composeView, params)
}
}
}
If it's possible to add touch gesture on parent you can check if touched position is in your composeView and check drag as
Result
@Preview
@Composable
fun DragFromCenterTest() {
var offset by remember {
mutableStateOf(Offset.Zero)
}
var childSize by remember {
mutableStateOf(IntSize.Zero)
}
var isTouched by remember {
mutableStateOf(false)
}
Box(
modifier = Modifier.fillMaxSize()
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
val position = down.position
isTouched = position.minus(offset)
.getDistanceSquared() < childSize.width * childSize.width
do {
//This PointerEvent contains details including
// event, id, position and more
val event: PointerEvent = awaitPointerEvent()
event.changes.firstOrNull()?.let { pointerInputChange ->
if (isTouched) {
val position = pointerInputChange.position
offset =
Offset(
position.x - childSize.width / 2,
position.y - childSize.height / 2
)
}
}
} while (event.changes.any { it.pressed })
}
// Alternative 2 with drag
// detectDragGestures(
// onDragStart = { position ->
// isTouched = position.minus(offset)
// .getDistanceSquared() < childSize.width * childSize.width
//
// if (isTouched) {
// offset =
// Offset(
// position.x - childSize.width / 2,
// position.y - childSize.height / 2
// )
// }
// },
// onDrag = { change, dragAmount ->
//
// val position = change.position
//
// if (isTouched) {
// offset =
// Offset(
// position.x - childSize.width / 2,
// position.y - childSize.height / 2
// )
// }
// }
// )
}
) {
Draggable(
modifier = Modifier
.onSizeChanged {
childSize = it
}
.offset {
IntOffset(offset.x.toInt(), offset.y.toInt())
}
.drawWithContent {
drawContent()
drawCircle(
color = Color.Red,
radius = 10.dp.toPx()
)
}
)
}
}
@Composable
fun Draggable(
modifier: Modifier,
) {
Box(modifier.size(100.dp).background(Color.Blue, CircleShape))
}
If you wish to add gesture to child you need to calculate distance of first touch to center of your Composable which is as
@Preview
@Composable
fun DragFromCenterTest2() {
var offset by remember {
mutableStateOf(Offset.Zero)
}
Box(
modifier = Modifier.fillMaxSize()
) {
Draggable(
modifier = Modifier
.offset {
IntOffset(offset.x.toInt(), offset.y.toInt())
}
.pointerInput(Unit) {
val size = size
val center = size.center
awaitEachGesture {
val down = awaitFirstDown()
val firstDown = down.position
val distanceToCenter =
Offset(firstDown.x - center.x, firstDown.y - center.y)
// Move current position to first down position to center it at first
// touch position
offset += distanceToCenter
do {
val event: PointerEvent = awaitPointerEvent()
event.changes.firstOrNull()?.let { pointerInputChange ->
val position = pointerInputChange.positionChange()
offset += position
}
} while (event.changes.any { it.pressed })
}
}
.drawWithContent {
drawContent()
drawCircle(
color = Color.Red,
radius = 10.dp.toPx()
)
}
)
}
}