简体   繁体   English

jetpack compose 中是否有等效的“触摸代表”?

[英]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. android 视图可以有一个触摸委托来增加元素的可点击区域,而不增加它的填充。 Is there something like that in jetpack compose? jetpack compose 中有类似的东西吗? I can't find a modifier that will do it.我找不到可以做到这一点的修饰符。

Move state to parent将状态移动到父级

@Composable
fun ViewScreen() {
    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    var rotation by remember { mutableStateOf(0f) }

    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            // touch event
            .pointerInput(Unit) {
                detectTransformGestures(
                    onGesture = { _, pan, gestureZoom, gestureRotate ->
                        scale *= gestureZoom
                        offset = offset.plus(pan)
                        rotation += gestureRotate
                    }
                )
            }
            .fillMaxSize()
    ) {
        Cube(scale, offset, rotation)
    }
}

@Composable
fun Cube(scale: Float, offset: Offset, rotation: Float) {
    Box(
        Modifier
            .graphicsLayer(
                scaleX = scale,
                scaleY = scale,
                rotationZ = rotation,
                translationX = offset.x,
                translationY = offset.y
            )
            .background(Color.Blue)
            .size(256.dp)
    )
}

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 1-) 创建数据类以增加或减少触摸区域的 dp 大小

@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 2-) 创建一个组合修饰符来恢复或记住它的状态

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. 3-) Modifier.graphicsLayer{}可以缩放、平移或旋转我们的 Composable 层。 And order of it matters, if we set it before Modifier.clickable it increases both clickable area and Composable scale.它的顺序很重要,如果我们在 Modifier.clickable 之前设置它,它会增加可点击区域和可组合比例。 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.上面的Modifier.graphicsLayer{}的职责是放大触摸区域和 Composable,缩放并转换回原始位置第二个Modifier.graphicsLayer{}是必需的。

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. 4-) 基本上我们缩放为新添加的 dp 大小并左移和上移,因为我们的变换原点通常是中心但更难计算平移。

5-) When translating back we need to consider reverse of scale. 5-) 向后平移时,我们需要考虑比例反转。

6-) To have accurate scaling we need size of our Composable which we get from Modifier.onSizeChanged{} 6-) 为了获得准确的缩放,我们需要从Modifier.onSizeChanged{}获得的 Composable 的大小

7-) Scale function that creates scale using initial non zero size and the size after we add touch offset via rectangle 7-) 使用初始非零大小和我们通过矩形添加触摸偏移后的大小创建比例的缩放功能

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结果

在此处输入图像描述

I was looking for a cleaner solution for this and stumbled upon ViewConfiguration class.我一直在为此寻找更清洁的解决方案,并偶然发现了ViewConfiguration class。 So basically clickable modifier relies upon LocalViewConfiguration.current to provide a default implementation.所以基本上可点击修饰符依赖LocalViewConfiguration.current来提供默认实现。 If you already know the size you want increase the touch area to, you can just provide a custom implementation of this class and override minimumTouchTargetSize to provide an updated size.如果您已经知道要将触摸区域增加到的大小,则只需提供此 class 的自定义实现并覆盖minimumTouchTargetSize以提供更新的大小。 Here is an example sublcass of View Configuration这是视图配置的示例子类

class CustomViewConfiguration(
    private val viewConfiguration: android.view.ViewConfiguration,
    private val size: Dp
) : ViewConfiguration {
    override val longPressTimeoutMillis: Long
        get() = 
    android.view.ViewConfiguration.getLongPressTimeout().toLong()

    override val doubleTapTimeoutMillis: Long
        get() = 
    android.view.ViewConfiguration.getDoubleTapTimeout().toLong()

    override val doubleTapMinTimeMillis: Long
        get() = 40

    override val touchSlop: Float
        get() = viewConfiguration.scaledTouchSlop.toFloat()

    override val minimumTouchTargetSize: DpSize
        get() = DpSize(size, size)
}

Once this is defined, you can provide this to your composable by using CompositionLocalProider一旦定义好,您可以使用CompositionLocalProider将其提供给您的可组合

CompositionLocalProvider(
    LocalViewConfiguration provides CustomViewConfiguration(
        android.view.ViewConfiguration.get(
            LocalContext.current
        ),
        size.dp
    )
) {
    // Your composable here that needs an increased size

}

If you don't know the increased size before hand, you can use BoxWithConstraints or any other SubComposeLayout which measures the composables and provides them in the constraints for you to use.如果您事先不知道增加的大小,您可以使用BoxWithConstraints或任何其他SubComposeLayout来测量可组合项并在约束中提供它们以供您使用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM