如何使用 detectorTransformGestures 但不消耗所有指针事件

问题描述 投票:0回答:5

我正在制作一个全屏照片查看器,其中包含一个寻呼机(使用

HorizontalPager
)和每个页面,用户可以放大/缩小和平移图像,但仍然可以滑动页面。

我的想法是,当图像未放大(比例因子= 1)时,会发生滑动页面,如果放大(比例因子> 1),则拖动/滑动将平移图像。

这是包含我的自定义可缩放图像的

HorizontalPager
的代码:

@ExperimentalPagerApi
@Composable
fun ViewPagerSlider(pagerState: PagerState, urls: List<String>) {


var scale = remember {
    mutableStateOf(1f)
}
var transX = remember {
    mutableStateOf(0f)
}
var transY = remember {
    mutableStateOf(0f)
}

HorizontalPager(
    count = urls.size,
    state = pagerState,
    modifier = Modifier
        .padding(0.dp, 40.dp, 0.dp, 40.dp),
) { page ->

    Image(
        painter = rememberImagePainter(
            data = urls[page],
            emptyPlaceholder = R.drawable.img_default_post,
        ),
        contentScale = ContentScale.FillHeight,
        contentDescription = null,
        modifier = Modifier
            .fillMaxSize()
            .graphicsLayer(
                translationX = transX.value,
                translationY = transY.value,
                scaleX = scale.value,
                scaleY = scale.value,
            )
            .pointerInput(scale.value) {
                detectTransformGestures { _, pan, zoom, _ ->
                    scale.value = when {
                        scale.value < 1f -> 1f
                        scale.value > 3f -> 3f
                        else -> scale.value * zoom
                    }
                    if (scale.value > 1f) {
                        transX.value = transX.value + (pan.x * scale.value)
                        transY.value = transY.value + (pan.y * scale.value)
                    } else {
                        transX.value = 0f
                        transY.value = 0f
                    }
                }
            }
    )
}
}

所以我的图像最大放大了3f,并且不能缩小到小于0。

如果

detectTransformGestures
在我的代码中,我无法滑动以更改到另一个页面。如果我根据因子放置
detectTransformGestures
(scale = 1,如果不放大则可以滑动到另一个页面),那么这将是一个“死锁”,因为我无法放大,因为没有侦听器。

我不知道是否有一些方法可以实现...

谢谢大家的宝贵时间!

android-jetpack-compose gesture pinchzoom
5个回答
9
投票

我必须做类似的事情,并想出了这个:

private fun ZoomableImage(
    modifier: Modifier = Modifier,
    bitmap: ImageBitmap,
    maxScale: Float = 1f,
    minScale: Float = 3f,
    contentScale: ContentScale = ContentScale.Fit,
    isRotation: Boolean = false,
    isZoomable: Boolean = true,
    lazyState: LazyListState
) {
    val scale = remember { mutableStateOf(1f) }
    val rotationState = remember { mutableStateOf(1f) }
    val offsetX = remember { mutableStateOf(1f) }
    val offsetY = remember { mutableStateOf(1f) }

    val coroutineScope = rememberCoroutineScope()
    Box(
        modifier = Modifier
            .clip(RectangleShape)
            .background(Color.Transparent)
            .combinedClickable(
                interactionSource = remember { MutableInteractionSource() },
                indication = null,
                onClick = { /* NADA :) */ },
                onDoubleClick = {
                    if (scale.value >= 2f) {
                        scale.value = 1f
                        offsetX.value = 1f
                        offsetY.value = 1f
                    } else scale.value = 3f
                },
            )
            .pointerInput(Unit) {
                if (isZoomable) {
                    forEachGesture {
                        awaitPointerEventScope {
                            awaitFirstDown()
                            do {
                                val event = awaitPointerEvent()
                                scale.value *= event.calculateZoom()
                                if (scale.value > 1) {
                                    coroutineScope.launch {
                                        lazyState.setScrolling(false)
                                    }
                                    val offset = event.calculatePan()
                                    offsetX.value += offset.x
                                    offsetY.value += offset.y
                                    rotationState.value += event.calculateRotation()
                                    coroutineScope.launch {
                                        lazyState.setScrolling(true)
                                    }
                                } else {
                                    scale.value = 1f
                                    offsetX.value = 1f
                                    offsetY.value = 1f
                                }
                            } while (event.changes.any { it.pressed })
                        }
                    }
                }
            }

    ) {
        Image(
            bitmap = bitmap,
            contentDescription = null,
            contentScale = contentScale,
            modifier = modifier
                .align(Alignment.Center)
                .graphicsLayer {
                    if (isZoomable) {
                        scaleX = maxOf(maxScale, minOf(minScale, scale.value))
                        scaleY = maxOf(maxScale, minOf(minScale, scale.value))
                        if (isRotation) {
                            rotationZ = rotationState.value
                        }
                        translationX = offsetX.value
                        translationY = offsetY.value
                    }
                }
        )
    }
}

它是可缩放的,可旋转的(如果你想要的话),如果图像放大则支持平移,支持双击放大和缩小,还支持在可滚动元素内使用。我还没有想出一个解决方案来限制用户可以平移图像多远。

它使用

combinedClickable
进行双击缩放,而不会干扰其他手势,并使用
pointerInput
进行缩放、平移和旋转。

它使用此扩展功能来控制

LazyListState
,但如果您需要它
ScrollState
,那么修改它以满足您的需求应该不难:

suspend fun LazyListState.setScrolling(value: Boolean) {
    scroll(scrollPriority = MutatePriority.PreventUserInput) {
        when (value) {
            true -> Unit
            else -> awaitCancellation()
        }
    }
}

请随意根据您的需要进行修改。


3
投票

HorizontalPager 的解决方案,在滑动时具有更好的用户体验:

val pagerState = rememberPagerState()
val scrollEnabled = remember { mutableStateOf(true) }
HorizontalPager(
   count = ,
   state = pagerState,
   userScrollEnabled = scrollEnabled.value,
) { }


@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ZoomablePagerImage(
    modifier: Modifier = Modifier,
    painter: Painter,
    scrollEnabled: MutableState<Boolean>,
    minScale: Float = 1f,
    maxScale: Float = 5f,
    contentScale: ContentScale = ContentScale.Fit,
    isRotation: Boolean = false,
) {
    var targetScale by remember { mutableStateOf(1f) }
    val scale = animateFloatAsState(targetValue = maxOf(minScale, minOf(maxScale, targetScale)))
    var rotationState by remember { mutableStateOf(1f) }
    var offsetX by remember { mutableStateOf(1f) }
    var offsetY by remember { mutableStateOf(1f) }
    val configuration = LocalConfiguration.current
    val screenWidthPx = with(LocalDensity.current) { configuration.screenWidthDp.dp.toPx() }
    Box(
        modifier = Modifier
            .clip(RectangleShape)
            .background(Color.Transparent)
            .combinedClickable(
                interactionSource = remember { MutableInteractionSource() },
                indication = null,
                onClick = { },
                onDoubleClick = {
                    if (targetScale >= 2f) {
                        targetScale = 1f
                        offsetX = 1f
                        offsetY = 1f
                        scrollEnabled.value = true
                    } else targetScale = 3f
                },
            )
            .pointerInput(Unit) {
                forEachGesture {
                    awaitPointerEventScope {
                        awaitFirstDown()
                        do {
                            val event = awaitPointerEvent()
                            val zoom = event.calculateZoom()
                            targetScale *= zoom
                            val offset = event.calculatePan()
                            if (targetScale <= 1) {
                                offsetX = 1f
                                offsetY = 1f
                                targetScale = 1f
                                scrollEnabled.value = true
                            } else {
                                offsetX += offset.x
                                offsetY += offset.y
                                if (zoom > 1) {
                                    scrollEnabled.value = false
                                    rotationState += event.calculateRotation()
                                }
                                val imageWidth = screenWidthPx * scale.value
                                val borderReached = imageWidth - screenWidthPx - 2 * abs(offsetX)
                                scrollEnabled.value = borderReached <= 0
                                if (borderReached < 0) {
                                    offsetX = ((imageWidth - screenWidthPx) / 2f).withSign(offsetX)
                                    if (offset.x != 0f) offsetY -= offset.y
                                }
                            }
                        } while (event.changes.any { it.pressed })
                    }
                }
            }

    ) {
        Image(
            painter = painter,
            contentDescription = null,
            contentScale = contentScale,
            modifier = modifier
                .align(Alignment.Center)
                .graphicsLayer {
                    this.scaleX = scale.value
                    this.scaleY = scale.value
                    if (isRotation) {
                        rotationZ = rotationState
                    }
                    this.translationX = offsetX
                    this.translationY = offsetY
                }
        )
    }
}

1
投票

如果您可以创建一个可变状态变量来跟踪缩放系数,则可以在缩放系数大于 1 时添加pointerInput 修饰符,并在缩放系数大于 1 时将其保留。像这样的东西:

var zoomFactorGreaterThanOne by remember { mutableStateOf(false) }

Image(
    painter = rememberImagePainter(
        data = urls[page],
        emptyPlaceholder = R.drawable.img_default_post,
    ),
    contentScale = ContentScale.FillHeight,
    contentDescription = null,
    modifier = Modifier
        .fillMaxSize()
        .graphicsLayer(
            translationX = transX.value,
            translationY = transY.value,
            scaleX = scale.value,
            scaleY = scale.value,
        )
        .run {
            if (zoomFactorGreaterThanOne != 1.0f) {
                this.pointerInput(scale.value) {
                    detectTransformGestures { _, pan, zoom, _ ->
                        zoomFactorGreaterThanOne = scale.value > 1
                        
                        scale.value = when {
                            scale.value < 1f -> 1f
                            scale.value > 3f -> 3f
                            else -> scale.value * zoom
                        }
                        if (scale.value > 1f) {
                            transX.value = transX.value + (pan.x * scale.value)
                            transY.value = transY.value + (pan.y * scale.value)
                        } else {
                            transX.value = 0f
                            transY.value = 0f
                        }
                    }
                }
            } else {
                this
            }
        }

)

1
投票

修复了之前answer存在的错误

原创:Android 画廊

修改器的扩展,支持点击和手势操作,而不会在像 HorizontalPager 这样的可滚动容器中相互重叠 在 HorizontalPager 的情况下,如果我们消耗所有手势,那么 Pager 的滑动手势也将被忽略;另一方面,如果我们不使用任何手势,就可以解决之前的问题,但会发现一个新问题:变换和点击手势都是重叠的,当用户想要缩放然后平移时,这会带来非常糟糕的用户体验图像,点击/双击手势也被注册

!(scrollEnabled 逻辑值更改应针对每种情况手动实现)

fun Modifier.tapAndGesture(
    key: Any? = Unit,
    onTap: ((Offset) -> Unit)? = null,
    onDoubleTap: ((Offset) -> Unit)? = null,
    onGesture: ((centeroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit)? = null,
    scrollEnabled: MutableState<Boolean> = mutableStateOf(false)
) = composed(
    factory = {
        val scope = rememberCoroutineScope()

        val gestureModifier = Modifier.pointerInput(key) {
            /**
             * [detectTransformGestures]
             * @author SmartToolFactory
             * href: https://github.com/SmartToolFactory/Compose-Extended-Gestures
             */
            detectTransformGestures(
                consume = false,
                onGesture = { centroid: Offset,
                              pan: Offset,
                              zoom: Float,
                              rotation: Float,
                              _: PointerInputChange,
                              changes: List<PointerInputChange> ->
                    scope.launch {
                        onGesture?.invoke(centroid, pan, zoom, rotation)
                    }
                    changes.forEach {
                        // Consume if scroll gestures are not possible
                        if (!scrollEnabled.value) it.consume()
                    }
                }
            )
        }
        val tapModifier = Modifier.pointerInput(key) {
            detectTapGestures(
                onDoubleTap = onDoubleTap,
                onTap = onTap
            )
        }
        then(gestureModifier.then(tapModifier))
    },
    inspectorInfo = debugInspectorInfo {
        name = "tapAndGesture"
        properties["key"] = key
        properties["onTap"] = onTap
        properties["onDoubleTap"] = onDoubleTap
        properties["onGesture"] = onGesture
    }
)

0
投票

作为一个选项,还可以在 HorizontalPager 本身中处理滑动和缩放事件。

        @Composable
    fun PicturesPager(
        urls: List<String>,
    

contentScale: ContentScale = ContentScale.Fit,
    modifier: Modifier = Modifier
) {
    val pagerState = rememberPagerState(pageCount = { urls.size })
    val canSwipe = remember { mutableStateOf(true) }
    var scale by remember { mutableStateOf(1f) }
    val offset = remember { mutableStateOf(Offset.Zero) }
    val size = remember { mutableStateOf(Size.Zero) }

    HorizontalPager(
        state = pagerState, modifier = modifier
            .pointerInput(Unit) {
                detectTransformGestures { _, pan, zoom, _ ->
                    val newScale = scale * zoom
                    scale = newScale.coerceAtLeast(1f)
                    // you can set value to == 1 but it's not always easy for the 
                    // user to zoom out image exactly to 1 so personally I 
                    // find setting it to 1.05 more convenient
                    canSwipe.value = scale < 1.05f
                        val maxOffsetX = (size.value.width * scale - size.value.width) / 2
                        val newOffsetX =
                            (offset.value.x + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
                        val maxOffsetY = (size.value.height * scale - size.value.height) / 2
                        val newOffsetY =
                            (offset.value.y + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
                        offset.value = Offset(newOffsetX, newOffsetY)
                    }
                // enable/disable page swipe based on scale factor
                }, userScrollEnabled = canSwipe.value
        ) { page ->
            ZoomableImage(
                imageUrl = urls[page],
                scale = scale,
                offset = offset,
                size = size,
                contentScale = contentScale
            )
        }
    }
    
    @OptIn(ExperimentalResourceApi::class)
    @Composable
        fun ZoomableImage(
            imageUrl: String,
            scale: Float,
            offset: MutableState<Offset>,
            size: MutableState<Size>,
            contentScale: ContentScale,
        ) {
            //The box here is not a requirement so you can remove it and set graphicsLayer to the image directly
            Box(
                modifier = Modifier
                    .graphicsLayer(
                        scaleX = maxOf(1f, scale),
                        scaleY = maxOf(1f, scale),
                        translationX = offset.value.x,
                        translationY = offset.value.y
                    )
            ) {
                KamelImage(
                    resource = asyncPainterResource(imageUrl),
                    contentDescription = stringResource(Res.string.hint_auction_image),
                    contentScale = contentScale,
                    // Here we are getting an updated image size and passing it back to HorizontalPager
                    modifier = Modifier.fillMaxWidth().onSizeChanged { newSize ->
                        size.value = Size(newSize.width.toFloat(), newSize.height.toFloat())
                    },
                    onLoading = { _ ->
                        CircularProgressIndicator()
                    }
                )
            }
        }
© www.soinside.com 2019 - 2024. All rights reserved.