【问题标题】:Better zooming behaviour in jetpack composeJetpack compose 中更好的缩放行为
【发布时间】:2022-04-02 06:11:56
【问题描述】:

默认缩放行为as explained in the compose documentation 会干扰拖动手势,并围绕可缩放的中心而不是您的手指进行旋转和缩放

有没有更好的方法来做到这一点?

【问题讨论】:

    标签: android kotlin android-jetpack-compose


    【解决方案1】:

    我将此解决方案中的代码制成了一个库:de.mr-pine.utils:zoomables

    你必须使用pointerInputScope with detectTransformGestures 和这个函数作为你的onGesture:

    fun onTransformGesture(
        centroid: Offset,
        pan: Offset,
        zoom: Float,
        transformRotation: Float
    ) {
        offset += pan
        scale *= zoom
        rotation += transformRotation
    
        val x0 = centroid.x - imageCenter.x
        val y0 = centroid.y - imageCenter.y
    
        val hyp0 = sqrt(x0 * x0 + y0 * y0)
        val hyp1 = zoom * hyp0 * (if (x0 > 0) {
            1f
        } else {
            -1f
        })
    
        val alpha0 = atan(y0 / x0)
    
        val alpha1 = alpha0 + (transformRotation * ((2 * PI) / 360))
    
        val x1 = cos(alpha1) * hyp1
        val y1 = sin(alpha1) * hyp1
    
        transformOffset =
            centroid - (imageCenter - offset) - Offset(x1.toFloat(), y1.toFloat())
        offset = transformOffset
    }
    

    这是一个如何围绕触摸输入旋转/缩放的示例,它还支持滑动和双击以重置缩放/放大:

    val scope = rememberCoroutineScope()
    
    var scale by remember { mutableStateOf(1f) }
    var rotation by remember { mutableStateOf(0f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
        scale *= zoomChange
        rotation += rotationChange
        offset += offsetChange
    }
    
    var dragOffset by remember { mutableStateOf(Offset.Zero) }
    var imageCenter by remember { mutableStateOf(Offset.Zero) }
    var transformOffset by remember { mutableStateOf(Offset.Zero) }
    
    
    Box(
        Modifier
            .pointerInput(Unit) {
                detectTapGestures(
                    onDoubleTap = {
                        if (scale != 1f) {
                            scope.launch {
                                state.animateZoomBy(1 / scale)
                            }
                            offset = Offset.Zero
                            rotation = 0f
                        } else {
                            scope.launch {
                                state.animateZoomBy(2f)
                            }
                        }
                    }
                )
            }
            .pointerInput(Unit) {
                val panZoomLock = true
                forEachGesture {
                    awaitPointerEventScope {
                        var transformRotation = 0f
                        var zoom = 1f
                        var pan = Offset.Zero
                        var pastTouchSlop = false
                        val touchSlop = viewConfiguration.touchSlop
                        var lockedToPanZoom = false
                        var drag: PointerInputChange?
                        var overSlop = Offset.Zero
    
                        val down = awaitFirstDown(requireUnconsumed = false)
    
    
                        var transformEventCounter = 0
                        do {
                            val event = awaitPointerEvent()
                            val canceled = event.changes.fastAny { it.positionChangeConsumed() }
                            var relevant = true
                            if (event.changes.size > 1) {
                                if (!canceled) {
                                    val zoomChange = event.calculateZoom()
                                    val rotationChange = event.calculateRotation()
                                    val panChange = event.calculatePan()
    
                                    if (!pastTouchSlop) {
                                        zoom *= zoomChange
                                        transformRotation += rotationChange
                                        pan += panChange
    
                                        val centroidSize =
                                            event.calculateCentroidSize(useCurrent = false)
                                        val zoomMotion = abs(1 - zoom) * centroidSize
                                        val rotationMotion =
                                            abs(transformRotation * PI.toFloat() * centroidSize / 180f)
                                        val panMotion = pan.getDistance()
    
                                        if (zoomMotion > touchSlop ||
                                            rotationMotion > touchSlop ||
                                            panMotion > touchSlop
                                        ) {
                                            pastTouchSlop = true
                                            lockedToPanZoom =
                                                panZoomLock && rotationMotion < touchSlop
                                        }
                                    }
    
                                    if (pastTouchSlop) {
                                        val eventCentroid = event.calculateCentroid(useCurrent = false)
                                        val effectiveRotation =
                                            if (lockedToPanZoom) 0f else rotationChange
                                        if (effectiveRotation != 0f ||
                                            zoomChange != 1f ||
                                            panChange != Offset.Zero
                                        ) {
                                            onTransformGesture(
                                                eventCentroid,
                                                panChange,
                                                zoomChange,
                                                effectiveRotation
                                            )
                                        }
                                        event.changes.fastForEach {
                                            if (it.positionChanged()) {
                                                it.consumeAllChanges()
                                            }
                                        }
                                    }
                                }
                            } else if (transformEventCounter > 3) relevant = false
                            transformEventCounter++
                        } while (!canceled && event.changes.fastAny { it.pressed } && relevant)
    
                        do {
                            val event = awaitPointerEvent()
                            drag = awaitTouchSlopOrCancellation(down.id) { change, over ->
                                change.consumePositionChange()
                                overSlop = over
                            }
                        } while (drag != null && !drag.positionChangeConsumed())
                        if (drag != null) {
                            dragOffset = Offset.Zero
                            if (scale !in 0.92f..1.08f) {
                                offset += overSlop
                            } else {
                                dragOffset += overSlop
                            }
                            if (drag(drag.id) {
                                    if (scale !in 0.92f..1.08f) {
                                        offset += it.positionChange()
                                    } else {
                                        dragOffset += it.positionChange()
                                    }
                                    it.consumePositionChange()
                                }
                            ) {
                                if (scale in 0.92f..1.08f) {
                                    val offsetX = dragOffset.x
                                    if (offsetX > 300) {
                                        onSwipeRight()
    
                                    } else if (offsetX < -300) {
                                        onSwipeLeft()
                                    }
                                }
                            }
                        }
                    }
                }
            }
    ) {
        ZoomComposable(
            modifier = Modifier
                .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
                .graphicsLayer(
                    scaleX = scale - 0.02f,
                    scaleY = scale - 0.02f,
                    rotationZ = rotation
                )
                .onGloballyPositioned { coordinates ->
                    val localOffset =
                        Offset(
                            coordinates.size.width.toFloat() / 2,
                            coordinates.size.height.toFloat() / 2
                        )
                    val windowOffset = coordinates.localToWindow(localOffset)
                    imageCenter = coordinates.parentLayoutCoordinates?.windowToLocal(windowOffset)
                        ?: Offset.Zero
                }
        )
    }
    

    【讨论】:

    • onTransformGesture 如何访问属性偏移、缩放、旋转、imageCenter 和 transformOffset?
    • 能否将这两个code-sn-ps组合成一个完整的Composeable-function以便代码编译?
    • @arne.jans 我一直在开发一个实现此功能的库,但最近几天还没有最终发布它。你可以查看实现它的源代码here。我会在发布时编辑此答案
    • 我期待着尝试一下,听起来很有希望!
    • 我昨天在一个以 GlideImage 为内容的 Horizo​​ntalPager 中试用了您的 Zoomables-composable,并向您的出色图书馆提出了一些问题和反馈!你愿意我在你的 github-repo 上创建一个或多个问题吗?
    【解决方案2】:

    这是一个非常简单的可缩放图像。

    @Composable
    fun ZoomableImage() {
        var scale by remember { mutableStateOf(1f) }
        var offset by remember { mutableStateOf(Offset.Zero) }
    
        Box(
            Modifier
                .size(600.dp)
        ) {
            Image(
                painter = rememberImagePainter(data = "https://picsum.photos/600/600"),
                contentDescription = "A Content description",
                modifier = Modifier
                    .align(Alignment.Center)
                    .graphicsLayer(
                        scaleX = scale,
                        scaleY = scale,
                        translationX = if (scale > 1f) offset.x else 0f,
                        translationY = if (scale > 1f) offset.y else 0f
                    )
                    .pointerInput(Unit) {
                        detectTransformGestures(
                            onGesture = { _, pan: Offset, zoom: Float, _ ->
                                offset += pan
                                scale = (scale * zoom).coerceIn(0.5f, 4f)
                            }
                        )
                    }
            )
        }
    }
    

    仅支持缩放和平移。旋转和双击不是。 为了使平移稍微平滑一些,您可以对pan 应用一个小的乘数,例如:

    offset += pan * 1.5f
    

    我还添加了coerceIn 以避免放大/缩小直到看起来奇怪的边界。如果需要,请随时删除 coerceIn。您还可以删除包含BoxAlignment。 仅当我们之前缩放时才会应用平移(平移)。恕我直言,这看起来更自然。

    欢迎反馈和改进

    【讨论】:

    • 如果您只需要基本的缩放和平移行为,这似乎是一个不错的解决方案。在这种情况下,您还可以使用 transformable Modifier 为您处理手势,支持旋转(但仅围绕 Composable 的中心)并且不会干扰双击
    • 这是我尝试@Mr.Pine 的第一件事,但可变形似乎没有实现平移,只能用捏手势放大/缩小。我喜欢可转换代码的外观,但不幸的是,我无法使用平移手势向上/向下/向左/向右移动。如果您有一个使用 transformable 的工作示例,我很乐意在我的设备上试用它
    • 所以我今天尝试了它,同时将我的解决方案变成了一个库,并且可转换支持平移。我在文档中尝试了this 示例,其中 pan 位于单独的偏移修改器中。 This example 建议它也应该在 graphicsLayer 修饰符中工作
    猜你喜欢
    • 1970-01-01
    • 2023-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-22
    • 1970-01-01
    • 1970-01-01
    • 2022-12-06
    相关资源
    最近更新 更多