Android Jetpack compose 版本的 PhotoView

Android Jetpack compose 下的简版 PhotoView

可双击放大、还原,可缩放、平移

/**
 * 放大倍数 a: 2倍, 放大倍数 b: 容器宽/图片宽、容器长/图片长的较大值.
 * 双击: 放大到倍数 min(a,b), 缩小到原始尺寸
 * 双指: 放大到倍数 max(a,b), 缩小到原始尺寸
 */
@Composable
fun PhotoView(click: (() -> Unit)? = null) {
    var imageInitialSize by remember {
        mutableStateOf(IntSize.Zero)
    }
    var boxSize by remember {
        mutableStateOf(IntSize.Zero)
    }

    var maxScale by remember {
        mutableStateOf(1f)
    }
    var scale by remember {
        mutableStateOf(1f)
    }
    var offset by remember {
        mutableStateOf(Offset.Zero)
    }
    // 使用 transformable 进行缩放、平移和旋转, 但是都需要双指, 不太适用图片放大后的平移(通常是单指)
    val transformState = rememberTransformableState { zoomChange, panChange, rotationChange ->
        scale = (scale * zoomChange).coerceIn(1f, maxScale)

        // 1. 直接使用平移距离
        // offset += panChange

        // 2. 考虑图片超出容器边界问题
        /*var x = offset.x
        if (scale * imageInitialSize.width > boxSize.width) {
            val delta = scale * imageInitialSize.width - boxSize.width
            x = (offset.x + panChange.x).coerceIn(-delta / 2, delta / 2)
        }
        var y = offset.y
        if (scale * imageInitialSize.height > boxSize.height) {
            val delta = scale * imageInitialSize.height - boxSize.height
            y = (offset.y + panChange.y).coerceIn(-delta / 2, delta / 2)
        }
        offset = Offset(x, y)*/
    }

    Box(modifier = Modifier
        .fillMaxSize()
        .graphicsLayer {
            scaleX = scale
            scaleY = scale
            translationX = offset.x
            translationY = offset.y
        }
        // 1. 使用 transformable 处理缩放和平移
        //.transformable(transformState)
        // 2. 使用 awaitPointerEventScope 自行处理缩放和平移
        .pointerInput(Unit) {
            forEachGesture {
                awaitPointerEventScope {
                    do {
                        val event = awaitPointerEvent()
                        val zoomChange = event.calculateZoom()
                        // LogUtils.d("zoomChange: $zoomChange")
                        scale *= zoomChange

                        var panChange = event.calculatePan()
                        // LogUtils.d("panChange: $panChange")
                        // 乘以 scale 防止在图片放大状态下移动得特别慢
                        panChange *= scale

                        // 为了让放大倍数超过 maxScale 时还可以随手势继续放大,
                        // 但是要手松开后回到 maxScale 这个状态, 在下面 awaitPointerEventScope 结束后
                        // 执行了 scale.coerceIn(1f, maxScale).
                        // 因此这个地方也要临时使用最后的真实值来计算 offset, 否则会出现整个图片上下 offset
                        // 不一致的情况(放大到超过 maxScale 后再松手)
                        val tempScale = scale.coerceIn(1f, maxScale)
                        var x = offset.x
                        if (tempScale * imageInitialSize.width > boxSize.width) {
                            val delta = tempScale * imageInitialSize.width - boxSize.width
                            x = (offset.x + panChange.x).coerceIn(-delta / 2, delta / 2)
                        }
                        var y = offset.y
                        if (tempScale * imageInitialSize.height > boxSize.height) {
                            val delta = tempScale * imageInitialSize.height - boxSize.height
                            y = (offset.y + panChange.y).coerceIn(-delta / 2, delta / 2)
                        }

                        if (x != offset.x || y != offset.y) {
                            offset = Offset(x, y)
                        }

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

                scale = scale.coerceIn(1f, maxScale)
                if (scale == 1f) {
                    offset = Offset.Zero
                }
            }
        }
        .pointerInput(Unit) {
            detectTapGestures(
                onDoubleTap = {
                    offset = Offset.Zero
                    if (scale != 1.0f) {
                        scale = 1.0f
                    } else {
                        scale = min(2f, maxScale)
                    }
                }, onTap = {
                    click?.invoke()
                }
            )
        }
        .onSizeChanged {
            // LogUtils.d("Box onSizeChanged")
            boxSize = it
            val xRatio = it.width.toFloat() / imageInitialSize.width
            val yRatio = it.height.toFloat() / imageInitialSize.height
            maxScale = max(2f, max(xRatio, yRatio))
        },
        contentAlignment = Alignment.Center
    ) {
        Image(
            modifier = Modifier.onSizeChanged {
                // LogUtils.d("Image onSizeChanged")
                imageInitialSize = it
            }, painter = painterResource(id = R.drawable.screenshot), contentDescription = ""
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PhotoViewPreview() {
    PhotoView()
}
使用示例.gif

你可能感兴趣的:(Android Jetpack compose 版本的 PhotoView)