An android view can have a touch delegate to increase the clickable area of an element, without increasing it's padding. Is there something like that in jetpack compose? I can't find a modifier that will do it.
This is implementation i build, i haven't countered any errors in calculations but if you do comment or hope it helps you build better one based on this answer.
1-) Create data classes to increase or decrease dp size of touch area
@Immutable
data class DelegateRect(
val left: Dp = 0.dp,
val top: Dp = 0.dp,
val right: Dp = 0.dp,
val bottom: Dp = 0.dp
) {
companion object {
val Zero = DelegateRect()
}
}
@Immutable
data class RectF(
val left: Float = 0f,
val top: Float = 0f,
val right: Float = 0f,
val bottom: Float = 0f
)
2-) Create a composed Modifier to restore or remember its state
fun Modifier.touchDelegate(
dpRect: DelegateRect = DelegateRect.Zero,
onClick: () -> Unit
) =
composed(
inspectorInfo = {
name = "touchDelegate"
properties["dpRect"] = dpRect
properties["onClick"] = onClick
},
factory = {
val density = LocalDensity.current
var initialSize by remember {
mutableStateOf(IntSize.Zero)
}
val updatedRect = remember(dpRect) {
with(density) {
RectF(
left = dpRect.left.toPx(),
top = dpRect.top.toPx(),
right = dpRect.right.toPx(),
bottom = dpRect.bottom.toPx(),
)
}
}
val scale = remember(initialSize, updatedRect) {
getScale(initialSize, updatedRect)
}
Modifier
.graphicsLayer {
scaleX = scale.x
scaleY = scale.y
this.translationX = -updatedRect.left
this.translationY = -updatedRect.top
transformOrigin = TransformOrigin(0f, 0f)
}
.clickable {
onClick()
}
.graphicsLayer {
val scaleX = if (scale.x == 0f) 1f else 1 / scale.x
val scaleY = if (scale.y == 0f) 1f else 1 / scale.y
this.scaleX = scaleX
this.scaleY = scaleY
this.translationX = (updatedRect.left) * scaleX
this.translationY = (updatedRect.top) * scaleY
transformOrigin = TransformOrigin(0f, 0f)
}
.onSizeChanged {
initialSize = it
}
}
)
Let me explain step by step
3-) Modifier.graphicsLayer{}
can scale, translate or rotate our Composable's layer. And order of it matters, if we set it before Modifier.clickable it increases both clickable area and Composable scale. Responsibility of Modifier.graphicsLayer{}
on top is to scale up touch area and Composable, to scale and translate back to original position second Modifier.graphicsLayer{}
is required.
4-) Basically we scale as new added dp size and translate left and top because our transform origin, normally it's center but more difficult to calculate translations.
5-) When translating back we need to consider reverse of scale.
6-) To have accurate scaling we need size of our Composable which we get from Modifier.onSizeChanged{}
7-) Scale function that creates scale using initial non zero size and the size after we add touch offset via rectangle
private fun getScale(initialSize: IntSize, updatedRect: RectF): Offset =
if (initialSize.width == 0 ||
initialSize.height == 0
) {
Offset(1f, 1f)
} else {
val initialWidth = initialSize.width
val initialHeight = initialSize.height
val scaleX =
((updatedRect.left + updatedRect.right + initialWidth) / initialWidth)
.coerceAtLeast(0f)
val scaleY =
((updatedRect.top + updatedRect.bottom + initialHeight) / initialHeight)
.coerceAtLeast(0f)
Offset(scaleX, scaleY)
}
Usage
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.size(200.dp)
.clickable { }
)
Spacer(modifier = Modifier.height(40.dp))
Image(
painter = painterResource(id = R.drawable.landscape1),
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.size(200.dp)
.touchDelegate(
DelegateRect(
left = 50.dp,
top = 40.dp,
right = 70.dp,
bottom = 90.dp
)
) {
}
)
}
Result
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With