Android Compose 实现图片预览(缩放、拖动、双击放大)

文章介绍了如何在JetpackCompose中处理单张图片的缩放、位移和旋转,特别是考虑到拖动边界限制。当图片拖动到屏幕边缘时,阻止进一步移动。同时,文章讨论了在HorizontalPager中嵌入这样的交互时遇到的问题,即pager无法正常滑动切换页面。为解决这个问题,作者提出了重写detectTransformGestures方法,增加返回值判断是否需要消费事件,以及如何在计算图片offset时判断手势的有效性,以确保手势可以正确传递到pager中。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       如果只是实现单张图片的缩放、位移、旋转可以参考官方文档直接使用transformable即可。

       在实际使用时会考虑拖动的边界,当图片拖到靠近屏幕边缘需要阻止,计算边界方法:

private fun calOffset(
    imgSize: Size,
    scale: Float,
    offsetChanged: Offset,
): Offset {
    if (imgSize == Size.Unspecified) return Offset.Zero
    val px = imgSize.width * (scale - 1f) / 2f
    val py = imgSize.height * (scale - 1f) / 2f
    var np = offsetChanged
    val xDiff = np.x.absoluteValue - px
    val yDiff = np.y.absoluteValue - py
    if (xDiff > 0)
        np = np.copy(x = px * np.x.absoluteValue / np.x)
    if (yDiff > 0)
        np = np.copy(y = py * np.y.absoluteValue / np.y)
    return np
}

在图片的Modifier中实现

.graphicsLayer {
    imgSize = size // 获取图片大小
    scaleX = scale
    scaleY = scale
    translationX = offset.x
    translationY = offset.y
}

双击放大则通过

.pointerInput() {
    detectTapGestures(
        onDoubleTap = { point ->
            scale = ...
            offset = ...
        }
    })
}

        但是当嵌入到HorizontalPager中后,pager就无法滑动切换页面了,在这种情况下就必须要了解手势的消费情况,想办法传递到pager中。
        要想处理手势就只能通过pointerInput里面的event来处理消费情况,里面有detectTransformGestures可以用来替换Modifier中的transformable,但是detectTransformGestures没有暴露event出来,我们通过源码可以看到event都被消费了:

suspend fun PointerInputScope.detectTransformGestures(
    panZoomLock: Boolean = false,
    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
) {
    awaitEachGesture {
        ...
        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)
            }
            event.changes.fastForEach {
                if (it.positionChanged()) {
                    it.consume() //此处被消费了
                }
            }
        }
        ...
    }
}

        所以此处最简单的的处理方式就是重写detectTransformGestures方法,将onGesture增加返回值,然后在消费之前判断是否需要被消费:

...
// 返回是否允许消费
allowConsume = onGesture(centroid, panChange, zoomChange, effectiveRotation)
...
event.changes.fastForEach {
    // 只有允许消费的才消费
    if (allowConsume && it.positionChanged()) {
        it.consume()
    }
}

 在计算图片offset时判断是否允许消费:
 

private fun calOffset(
    imgSize: Size,
    scale: Float,
    offsetChanged: Offset,
    isInvalid: (Boolean) -> Unit = {} // true:将手势交还父组件(pager)
): Offset {
    ...
    // 当横向移动超出边界,且横向移动大于纵向移动时,对子组件属于无效手势,此时将手势给父组件
    isInvalid(xDiff > 0 && xDiff > yDiff) 
    ...
}

 完整预览的代码:

// 如果是多张图片,page对应外层pager页码
// 如果是单张图片不用传pagerScope
@OptIn(ExperimentalPagerApi::class)
@Composable
fun PhotoPreview(data: Any?, page: Int = 0, pagerScope: PagerScope? = null) {
    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    var imgSize: Size = Size.Unspecified
    Surface(modifier = Modifier.fillMaxSize(), color = Color.Black) {
        AsyncImage(model = data, contentDescription = null, contentScale = ContentScale.Fit,
            modifier = Modifier
                .graphicsLayer {
                    imgSize = size
                    scaleX = scale
                    scaleY = scale
                    translationX = offset.x
                    translationY = offset.y
                    // 页面切换时需要恢复scale、offset
                    if (pagerScope != null) {
                        val pageOffset =
                            pagerScope.calculateCurrentOffsetForPage(page = page).absoluteValue
                        if (pageOffset == 1f) {
                            scale = 1f
                            offset = Offset.Zero
                        }
                    }
                }
                .pointerInput("transform") {
                    // 此处为自定义手势检测,主要是增加返回值判断是否需要消费
                    detectCustomTransformGestures { _, pan, zoom, _ ->
                        var requestIgnoreConsume = false
                        scale = (zoom * scale).coerceAtLeast(1f)
                        scale = if (scale > 5f) 5f else scale
                        offset = calOffset(imgSize, scale, offset + pan) {
                            requestIgnoreConsume = it
                        }
                        return@detectCustomTransformGestures requestIgnoreConsume
                    }
                }
                .pointerInput("tap") {
                    detectTapGestures(
                        onDoubleTap = { point ->
                            val center = Offset(imgSize.width / 2, imgSize.height / 2)
                            val realPoint = offset + center - point
                            scale = if (scale <= 1f) 2f else 1f
                            offset = if (scale <= 1f) Offset.Zero else {
                                calOffset(imgSize, scale, realPoint * 2f)
                            }
                        }
                    )
                })
    }
}

private fun calOffset(
    imgSize: Size,
    scale: Float,
    offsetChanged: Offset,
    isInvalid: (Boolean) -> Unit = {}
): Offset {
    if (imgSize == Size.Unspecified) return Offset.Zero
    val px = imgSize.width * (scale - 1f) / 2f
    val py = imgSize.height * (scale - 1f) / 2f
    var np = offsetChanged
    val xDiff = np.x.absoluteValue - px
    val yDiff = np.y.absoluteValue - py
    if (xDiff > 0)
        np = np.copy(x = px * np.x.absoluteValue / np.x)
    if (yDiff > 0)
        np = np.copy(y = py * np.y.absoluteValue / np.y)
    isInvalid(xDiff > 0 && xDiff > yDiff)
    return np
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值