android 如何在Jetpack Compose中处理水平滚动手势和变换手势

eyh26e7m  于 2023-06-04  发布在  Android
关注(0)|答案(1)|浏览(257)

嗨,我想在一个水平寻呼机有一个可缩放的图像。我已经实现了转换手势和双击,但现在,我不能滚动我的寻呼机与滚动的图像。我猜平移和滚动是相互混合的。你能帮我吗?

  • 我试着检测水平拖动手势,但它只能防止水平平移,至少我只能管理这一部分。
    这是我的可缩放图片合成
@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)
            }
    )
}

下面是我在水平分页中的用法:

@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()
        }
    }
}
pkln4tw6

pkln4tw61#

在jetpack中,默认情况下,通过默认传递(PointerEventPass.Main)将事件从后代传播到祖先。如果手势被消耗并且父/子检查它是否在它从未得到该事件之前被消耗。
您可以参考this answer了解更多详细信息。
默认情况下,detectTransformGestures在发生移动事件时消耗PointerInputChange秒。为了克服这个问题,您可以添加一个可选参数来有条件地使用,例如缩放大于1或向下指针的数量大于1。
我写了一个detectTransformGesture,它检查消耗,并返回屏幕上的指针

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)
    }
}

从上面的代码片段可以看出,只有当consume参数设置为true时,我才消费change

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

相关问题