简体   繁体   中英

How to use detectTransformGestures but not consuming all pointer event

I was making a fullscreen photo viewer which contain a pager (used HorizontalPager ) and each page, user can zoom in/out and pan the image, but still able to swipe through pages.

My idea is swiping page will occurs when the image is not zoomed in (scale factor = 1), if it's zoomed in (scale factor > 1) then dragging/swiping will pan the image around.

Here is the code for the HorizontalPager that contain my customized zoomable Image:

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

So my image is zoomed in maximum 3f, and cannot zoom out smaller than 0.

I cannot swipe to change to another page if detectTransformGestures is in my code. If I put the detectTransformGestures based on the factor (scale = 1, make it swipeable to another page if not zoomed in), then it will be a "deadlock" as I cannot zoom in because there is no listener.

I don't know if there is some how to make it possible...

Thank you guys for your time!

I had to do something similar, and came up with this:

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

It is zoomable, rotatable (if you want it), supports pan if the image is zoomed in, has support for double-click zoom-in and zoom-out and also supports being used inside a scrollable element. I haven't come up with a solution to limit how far can the user pan the image yet.

It uses combinedClickable so the double-click zoom works without interfering with the other gestures, and pointerInput for the zoom, pan and rotation.

It uses this extension function to control the LazyListState , but if you need it for ScrollState it shouldn't be hard to modify it to suit your needs:

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

Feel free to modify it for your needs.

Solution for HorizontalPager with better ux on swipe:

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

If you can create a mutable state variable that keeps track of the zoom factor, you can add the pointerInput modifier when the zoom factor is greater than one and leave it out when it is greater than one. Something like this:

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

)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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