嗨,我想有一个可zoom 的图像在水平分页机.我已经实现了变换手势和双击,但现在,我不能滚动我的pager 滚动图像.我想平移和滚动是相互混淆的.你能帮我个忙吗?

  • 我试着检测水平拖动手势,但它只阻止水平平移,至少我可以做到这一部分.

This is my Zoomable Image Composable:


@Composable
fun DoubleTapZoom(
) {
    var zoomed by remember { mutableStateOf(false) }
    var transforming by remember { mutableStateOf(false) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    var zoomFloat by remember { mutableStateOf(1f) }
    var angle by remember { mutableStateOf(0f) }

    Image(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),
        contentDescription = null,
        modifier = Modifier
            .size(500.dp)
            .background(Color.Red)
            .pointerInput(Unit) {

                detectTapGestures(
                    onDoubleTap = { tapOffset ->
                        offset = if (zoomed) Offset.Zero else
                            calculateOffsetFromClick(size, tapOffset)
                        zoomed = !zoomed
                    }
                )
            }
            .pointerInput(Unit) {

                detectTransformGestures { centroid, pan, gestureZoom, gestureRotate ->
                    transforming = true
                    val oldScale = zoomFloat
                    val newScale = zoomFloat * gestureZoom
                    
                    offset = (offset + centroid / oldScale).rotateBy(gestureRotate) -
                            (centroid / newScale + pan / oldScale)
                    zoomFloat = newScale
                    angle += gestureRotate
                    transforming = false
                }
            }
            .graphicsLayer {
                scaleX = zoomFloat
                scaleY = zoomFloat
                translationX = -offset.x * zoomFloat
                translationY = -offset.y * zoomFloat
                rotationZ = angle
                transformOrigin = TransformOrigin(0f, 0f)
            }
    )
}

And here is the my usage in horizontal pager:


@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HorizontalPagerWithZoomableContent() {
    val pagerState = rememberPagerState()
    
    HorizontalPager(
        state = pagerState,
        modifier = Modifier
            .border(4.dp, Color.Red, RoundedCornerShape(36.dp))
            .clip(RoundedCornerShape(36.dp)),
        pageCount = 4,
    ) { pageIndex ->
        Box (modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center){

            DoubleTapZoom()
        }
    }
}

推荐答案

在Jetpack Compose中,默认情况下,事件通过默认传递(PointerEventPass.Main)从后代传播到祖先.如果手势被使用,则父/子判断该手势在收到该事件之前是否已被使用.

你可以参考this answer了解更多细节.

默认情况下,detectTransformGestures在发生移动事件时消耗PointerInputChange秒.要克服这一点,您可以添加一个可选参数来有条件地使用,例如zoom 大于1或指针数大于1.

我编写了一个检测TransformGesture来判断消费,并将屏幕上显示的指针返回为

suspend fun PointerInputScope.detectCustomTransformGestures(
    panZoomLock: Boolean = false,
    consume: Boolean = true,
    pass: PointerEventPass = PointerEventPass.Main,
    onGestureStart: (PointerInputChange) -> Unit = {},
    onGesture: (
        centroid: Offset,
        pan: Offset,
        zoom: Float,
        rotation: Float,
        mainPointer: PointerInputChange,
        changes: List<PointerInputChange>
    ) -> Unit,
    onGestureEnd: (PointerInputChange) -> Unit = {}
) {
    awaitEachGesture {
        var rotation = 0f
        var zoom = 1f
        var pan = Offset.Zero
        var pastTouchSlop = false
        val touchSlop = viewConfiguration.touchSlop
        var lockedToPanZoom = false

        // Wait for at least one pointer to press down, and set first contact position
        val down: PointerInputChange = awaitFirstDown(
            requireUnconsumed = false,
            pass = pass
        )
        onGestureStart(down)

        var pointer = down
        // Main pointer is the one that is down initially
        var pointerId = down.id

        do {
            val event = awaitPointerEvent(pass = pass)

            // If any position change is consumed from another PointerInputChange
            // or pointer count requirement is not fulfilled
            val canceled =
                event.changes.any { it.isConsumed }

            if (!canceled) {

                // Get pointer that is down, if first pointer is up
                // get another and use it if other pointers are also down
                // event.changes.first() doesn't return same order
                val pointerInputChange =
                    event.changes.firstOrNull { it.id == pointerId }
                        ?: event.changes.first()

                // Next time will check same pointer with this id
                pointerId = pointerInputChange.id
                pointer = pointerInputChange

                val zoomChange = event.calculateZoom()
                val rotationChange = event.calculateRotation()
                val panChange = event.calculatePan()

                if (!pastTouchSlop) {
                    zoom *= zoomChange
                    rotation += rotationChange
                    pan += panChange

                    val centroidSize = event.calculateCentroidSize(useCurrent = false)
                    val zoomMotion = abs(1 - zoom) * centroidSize
                    val rotationMotion =
                        abs(rotation * kotlin.math.PI.toFloat() * centroidSize / 180f)
                    val panMotion = pan.getDistance()

                    if (zoomMotion > touchSlop ||
                        rotationMotion > touchSlop ||
                        panMotion > touchSlop
                    ) {
                        pastTouchSlop = true
                        lockedToPanZoom = panZoomLock && rotationMotion < touchSlop
                    }
                }

                if (pastTouchSlop) {
                    val centroid = event.calculateCentroid(useCurrent = false)
                    val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange
                    if (effectiveRotation != 0f ||
                        zoomChange != 1f ||
                        panChange != Offset.Zero
                    ) {
                        onGesture(
                            centroid,
                            panChange,
                            zoomChange,
                            effectiveRotation,
                            pointer,
                            event.changes
                        )
                    }

                    if (consume) {
                        event.changes.forEach {
                            if (it.positionChanged()) {
                                it.consume()
                            }
                        }
                    }
                }
            }
        } while (!canceled && event.changes.any { it.pressed })
        onGestureEnd(pointer)
    }
}

从上面的代码片断可以看出,只有当Consumer Param设置为True时,我才会使用更改

if (consume) {
    event.changes.forEach {
        if (it.positionChanged()) {
            it.consume()
        }
    }
}

与默认手势不同.

此手势可用作

@Composable
fun DoubleTapZoom(
) {
    var zoomed by remember { mutableStateOf(false) }
    var transforming by remember { mutableStateOf(false) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    var zoomFloat by remember { mutableStateOf(1f) }
    var angle by remember { mutableStateOf(0f) }
    
    val context = LocalContext.current

    Image(
        painter = painterResource(id = R.drawable.ic_launcher_foreground),
        contentDescription = null,
        modifier = Modifier
            .size(500.dp)
            .background(Color.Red)
            .pointerInput(Unit) {

                detectTapGestures(
                    onDoubleTap = { tapOffset ->
                        Toast
                            .makeText(context, "DoubleTap", Toast.LENGTH_SHORT)
                            .show()
//                        offset = if (zoomed) Offset.Zero else
//                        calculateOffsetFromClick(size, tapOffset)
//                        zoomed = !zoomed
                    }
                )
            }
            .pointerInput(Unit) {

                detectCustomTransformGestures(
                    consume = false,
                    onGesture = { centroid, pan, gestureZoom, gestureRotate, _, changes ->
                        transforming = true
                        val oldScale = zoomFloat
                        val newScale = zoomFloat * gestureZoom

                        offset = (offset + centroid / oldScale).rotateBy(gestureRotate) -
                                (centroid / newScale + pan / oldScale)
                        zoomFloat = newScale
                        angle += gestureRotate
                        transforming = false

                        // If more than 1 pointer is down consume event
                        // to prevent Pager from scrolling
                        if (changes.size > 1) {
                            changes.forEach { it.consume() }
                        }
                    }
                )
            }
            .graphicsLayer {
                scaleX = zoomFloat
                scaleY = zoomFloat
                translationX = -offset.x * zoomFloat
                translationY = -offset.y * zoomFloat
                rotationZ = angle
                transformOrigin = TransformOrigin(0f, 0f)
            }
    )
}

也可用作手势库,扩展此处提供的默认手势的功能

https://github.com/SmartToolFactory/Compose-Extended-Gestures/tree/master/gesture

Android相关问答推荐

如何使用单个代码库使用不同的firebase项目创建多个应用程序ID apk

Jetpack编写使用自定义主题覆盖库中主题部分

避免在按下肯定按钮时自动取消AlertDialog

如何从Android 12的来电中获取电话号码?

如何将我的Android应用程序(Kotlin)中的图像分享给其他应用程序?

在一列中垂直对齐两个框

如何使用进度条和返回函数进行API调用,同时在Android上使用Kotlin保持高效?

activity在 Runnable 中如何工作?我的 Android 表格未显示

setContentView() 方法的签名

Kotlin 协程、 retrofit 、android

当手表与手机断开连接时,有没有办法在我的 Wear OS 应用程序中显示操作系统级别的图标?

在事件中使用 Context/Toast 时不需要的重组 - Jetpack Compose

如何在 Android 应用中录制短视频?

Android 12 通过包管理器中断 APK 安装?

优化 Room 数据库迁移

Android Studio:如何添加应用程序质量洞察窗口以查看 Android Studio 中的 Crashlytics 数据?

在 Room 中创建一对多关系时,@Relation 类是什么意思?

如何在 Jetpack Compose 中擦除画布时变得透明,现在我得到白色?

WindowManager 内的 RecyclerView 不更新

创建后文件不存在