Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a 'touch delegate' equivalent in jetpack compose?

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.

like image 670
JCraw Avatar asked Sep 09 '25 23:09

JCraw


1 Answers

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

enter image description here

like image 86
Thracian Avatar answered Sep 12 '25 13:09

Thracian