Android Jetpack Compose:如何在“框”中缩放图像?

17

我打算在一个框内建立一个可缩放的图像视图,就像第一个截图一样。但是当我放大图片时,它将超出框的范围。有没有办法缩放图像,但保持大小不变?除了 view 或者 fragment,只有 box 似乎不足够。我期望图像会变大,但仍然保持在红色框内,但是我在缩放后得到了第二个截图。

缩放前 缩放后

感谢 nglauber 和 Amirhosein ,我用以下代码获得了同时具有缩放和拖动功能的“框”(固定区域)内最终解决方案,如下图所示。

        val imageBitmap = imageResource(id = R.drawable.android)
        Image(
            modifier = Modifier
                .preferredSize(400.dp, 300.dp)
                .clip(RectangleShape)
                .zoomable(onZoomDelta = { scale.value *= it })
                .rawDragGestureFilter(
                    object : DragObserver {
                        override fun onDrag(dragDistance: Offset): Offset {
                            translate.value = translate.value.plus(dragDistance)
                            return super.onDrag(dragDistance)
                        }
                    })
                .graphicsLayer(
                    scaleX = scale.value,
                    scaleY = scale.value,
                    translationX = translate.value.x,
                    translationY = translate.value.y
                ),
            contentDescription = null,
            bitmap = imageBitmap
        )

在此输入图像描述


2
你能够限制 translateXtranslateY 以使图像保持在框内吗? - Bugdr0id
1
Modifier.clipToBounds()clip(RectangleShape)可以完成此任务@Bugdr0id。 - joyl1216
6个回答

31

这里是我的解决方案... 或许对某些人有帮助...

@Composable
fun ZoomableImage() {
    val scale = remember { mutableStateOf(1f) }
    val rotationState = remember { mutableStateOf(1f) }
    Box(
        modifier = Modifier
            .clip(RectangleShape) // Clip the box content
            .fillMaxSize() // Give the size you want...
            .background(Color.Gray)
            .pointerInput(Unit) {
                detectTransformGestures { centroid, pan, zoom, rotation ->
                    scale.value *= zoom
                    rotationState.value += rotation
                }
            }
    ) {
        Image(
            modifier = Modifier
                .align(Alignment.Center) // keep the image centralized into the Box
                .graphicsLayer(
                    // adding some zoom limits (min 50%, max 200%)
                    scaleX = maxOf(.5f, minOf(3f, scale.value)),
                    scaleY = maxOf(.5f, minOf(3f, scale.value)),
                    rotationZ = rotationState.value
                ),
            contentDescription = null,
            painter = painterResource(R.drawable.dog)
        )
    }
}

它对我有效,即使将其放置在“Column”或其他布局中。谢谢! - joyl1216
1
“.clip(RectangleShape)”是我错过的关键点!你的版本只支持缩放,不支持拖动。” - joyl1216
3
非常有用,谢谢!提醒一下,rotationState 应该最初设为 0f - Baptiste Candellier
1
只是一点小改进,但更易读和理解:不要使用maxOf(min, minOf(max, scale.value)),您可以使用scale.value.coerceIn(min, max) - Sven Jacobs

10

我有一个更通用的解决方案,支持平移。这是基于nglauber和arun-padiyan提供的答案。

@Composable
fun ZoomableBox(
    modifier: Modifier = Modifier,
    minScale: Float = 0.1f,
    maxScale: Float = 5f,
    content: @Composable ZoomableBoxScope.() -> Unit
) {
    var scale by remember { mutableStateOf(1f) }
    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }
    var size by remember { mutableStateOf(IntSize.Zero) }
    Box(
        modifier = modifier
            .clip(RectangleShape)
            .onSizeChanged { size = it }
            .pointerInput(Unit) {
                detectTransformGestures { _, pan, zoom, _ ->
                    scale = maxOf(minScale, minOf(scale * zoom, maxScale))
                    val maxX = (size.width * (scale - 1)) / 2
                    val minX = -maxX
                    offsetX = maxOf(minX, minOf(maxX, offsetX + pan.x))
                    val maxY = (size.height * (scale - 1)) / 2
                    val minY = -maxY
                    offsetY = maxOf(minY, minOf(maxY, offsetY + pan.y))
                }
            }
    ) {
        val scope = ZoomableBoxScopeImpl(scale, offsetX, offsetY)
        scope.content()
    }
}

interface ZoomableBoxScope {
    val scale: Float
    val offsetX: Float
    val offsetY: Float
}

private data class ZoomableBoxScopeImpl(
    override val scale: Float,
    override val offsetX: Float,
    override val offsetY: Float
) : ZoomableBoxScope

使用方法类似于:
ZoomableBox {
    Image(
        modifier = Modifier
                    .graphicsLayer(
                        scaleX = scale,
                        scaleY = scale,
                        translationX = offsetX,
                        translationY = offsetY
                    ),
        bitmap = bitmap,
        contentDescription = null
    )
}

7

zoomable已经被弃用,可以使用PointerInputScope.detectTransformGestures

@Composable
fun ImagePreview(link: String) {
    Box(modifier = Modifier.fillMaxSize()) {
        var angle by remember { mutableStateOf(0f) }
        var zoom by remember { mutableStateOf(1f) }
        var offsetX by remember { mutableStateOf(0f) }
        var offsetY by remember { mutableStateOf(0f) }

        CoilImage(
            data = link,
            contentDescription = "image",
            contentScale = ContentScale.Fit,
            modifier = Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .graphicsLayer(
                    scaleX = zoom,
                    scaleY = zoom,
                    rotationZ = angle
                )
                .pointerInput(Unit) {
                    detectTransformGestures(
                        onGesture = { _, pan, gestureZoom, gestureRotate ->
                            angle += gestureRotate
                            zoom *= gestureZoom
                            val x = pan.x * zoom
                            val y = pan.y * zoom
                            val angleRad = angle * PI / 180.0
                            offsetX += (x * cos(angleRad) - y * sin(angleRad)).toFloat()
                            offsetY += (x * sin(angleRad) + y * cos(angleRad)).toFloat()
                        }
                    )
                }
                .fillMaxSize()
        )
    }
}

它不能正确地缩小,否则它的工作符合预期。 - Shenanigans1
当最大化缩放时 - Shenanigans1

3
使用 Jetpack Compose 中的 AsyncImage,来自于 coil。 从 nglauber 复制代码并根据我的要求进行了调整。 添加缩放/平移屏幕溢出限制。无图像旋转。 我不想添加依赖项仅仅是为了显示可访问的图片。

@Composable
fun ImagePreview(model: Any, contentDescription: String? = null) {
    Box(modifier = Modifier.fillMaxSize()) {
        val angle by remember { mutableStateOf(0f) }
        var zoom by remember { mutableStateOf(1f) }
        var offsetX by remember { mutableStateOf(0f) }
        var offsetY by remember { mutableStateOf(0f) }

        val configuration = LocalConfiguration.current
        val screenWidth = configuration.screenWidthDp.dp.value
        val screenHeight = configuration.screenHeightDp.dp.value

        AsyncImage(
            model,
            contentDescription = contentDescription,
            contentScale = ContentScale.Fit,
            modifier = Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .graphicsLayer(
                    scaleX = zoom,
                    scaleY = zoom,
                    rotationZ = angle
                )
                .pointerInput(Unit) {
                    detectTransformGestures(
                        onGesture = { _, pan, gestureZoom, _ ->
                            zoom = (zoom * gestureZoom).coerceIn(1F..4F)
                            if (zoom > 1) {
                                val x = (pan.x * zoom)
                                val y = (pan.y * zoom)
                                val angleRad = angle * PI / 180.0

                                offsetX =
                                    (offsetX + (x * cos(angleRad) - y * sin(angleRad)).toFloat()).coerceIn(
                                        -(screenWidth * zoom)..(screenWidth * zoom)
                                    )
                                offsetY =
                                    (offsetY + (x * sin(angleRad) + y * cos(angleRad)).toFloat()).coerceIn(
                                        -(screenHeight * zoom)..(screenHeight * zoom)
                                    )
                            } else {
                                offsetX = 0F
                                offsetY = 0F
                            }
                        }
                    )
                }
                .fillMaxSize()
        )
    }
}

2

我认为使用所有响应是最好的结果,包括:

  • 双击缩放
  • 手势缩放
  • 滚动
  • 关闭按钮
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ZoomableImage(
    model: Any,
    contentDescription: String? = null,
    onBackHandler: () -> Unit,
) {
    val angle by remember { mutableStateOf(0f) }
    var zoom by remember { mutableStateOf(1f) }
    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }

    val configuration = LocalConfiguration.current
    val screenWidth = configuration.screenWidthDp.dp.value
    val screenHeight = configuration.screenHeightDp.dp.value

    BackHandler { onBackHandler() }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black)
            .combinedClickable(
                interactionSource = remember { MutableInteractionSource() },
                indication = null,
                onClick = {},
                onDoubleClick = {
                    zoom = if (zoom > 1f) 1f
                    else 3f
                }
            )
    ) {
        AsyncImage(
            model,
            contentDescription = contentDescription,
            contentScale = ContentScale.Fit,
            modifier = Modifier
                .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                .graphicsLayer(
                    scaleX = zoom,
                    scaleY = zoom,
                    rotationZ = angle
                )
                .pointerInput(Unit) {
                    detectTransformGestures(
                        onGesture = { _, pan, gestureZoom, _ ->
                            zoom = (zoom * gestureZoom).coerceIn(1F..4F)
                            if (zoom > 1) {
                                val x = (pan.x * zoom)
                                val y = (pan.y * zoom)
                                val angleRad = angle * PI / 180.0

                                offsetX =
                                    (offsetX + (x * cos(angleRad) - y * sin(angleRad)).toFloat()).coerceIn(
                                        -(screenWidth * zoom)..(screenWidth * zoom)
                                    )
                                offsetY =
                                    (offsetY + (x * sin(angleRad) + y * cos(angleRad)).toFloat()).coerceIn(
                                        -(screenHeight * zoom)..(screenHeight * zoom)
                                    )
                            } else {
                                offsetX = 0F
                                offsetY = 0F
                            }
                        }
                    )
                }
                .fillMaxSize()
        )
        IconButton(
            onClick = onBackHandler
        ) {
            Icon(
                modifier = Modifier.size(18.dp),
                painter = painterResource(id = R.drawable.ic_close), // Replace  it
                contentDescription = "Close full screen",
                tint = Color.White
            )
        }
    }
}

1

只需在 Image 上设置 zoomablerawDragGestureFilter,而不是在 Box 上设置:

@Preview
@Composable
fun Zoomable(){
val scale = remember { mutableStateOf(1f) }
val translate = remember { mutableStateOf(Offset(0f, 0f)) }

Box(
    modifier = Modifier.preferredSize(300.dp)

) {
    val imageBitmap = imageResource(id = R.drawable.cover)
    Image(
        modifier = Modifier
            .zoomable(onZoomDelta = { scale.value *= it })
            .rawDragGestureFilter(
                object : DragObserver {
                    override fun onDrag(dragDistance: Offset): Offset {
                        translate.value = translate.value.plus(dragDistance)
                        return super.onDrag(dragDistance)
                    }
                })
            .graphicsLayer(
                scaleX = scale.value,
                scaleY = scale.value,
                translationX = translate.value.x,
                translationY = translate.value.y
            ),
        contentDescription = null,
        bitmap = imageBitmap
    )
  }
}

谢谢你的回复,但是即使按照你的更改,它仍然以相同的方式运作,在缩放时会超出范围。 - joyl1216
你是否为 Box 设置了静态大小? - Amirhosein
我无论如何都在使用Jetpack Compose 1.0.0-alpha11. - joyl1216
我找到了原因。我把Box放在了Column里面,这就是为什么它停止工作的原因。如果只有一个Box作为root布局,它可以工作。但是,仅仅因为将其放入其他布局中而破坏Box仍然没有任何意义。 - joyl1216
不错的解决方案,但还缺少一些调整。如何限制拖动以不离开屏幕?使用当前的解决方案,用户可以完全将图像拖出正方形。 - Bugdr0id
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接