Android 使用手势合成 Canvas 缩放图像,使其远离图像

Android compose Canvas scale image with gesture offset its far away from image

提问人:Rofie Sagara 提问时间:11/14/2023 更新时间:11/16/2023 访问量:55

问:

这个赏金已经结束了。这个问题的答案有资格获得 +50 声望赏金。赏金宽限期在 19 小时后结束。Rofie Sagara 正在寻找来自信誉良好的来源的答案

我创建了裁剪图像,用画布圈起来,它在裁剪旋转、缩放和移动图像方面效果很好,但是当我尝试将图像向右或向左移动时,问题就来了,而在比例 1,4 上照片的偏移量太大,所以图像不再在圆圈内,但是当我缩放更大的数字时,比如 3,它离图像更远, 我想让用户在运动图像时不会偏移太远,直到完全不在圆圈中,但我仍然希望用户能够到达图像的边缘。

这是 Compose 文件

@Composable
fun ZoomableImage(
    modifier: Modifier = Modifier,
    imageUrl: Uri,
    maxWidthVal: Int = 0,
    maxHeightVal: Int = 0,
) {
    var scale by remember { mutableStateOf(1f) }
    var offset by remember { mutableStateOf(Offset(0f, 0f)) }
    var rotation by remember { mutableStateOf(0f) }
    var croppedImage: Bitmap? by remember { mutableStateOf(null) }
    val minScale = 1f
    val maxScale = 3f
    var canvasSize by remember { mutableStateOf(Size(0f, 0f)) }

    var originalImageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }

    val context = LocalContext.current
    val imageLoader = ImageLoader(context)

    val request = ImageRequest.Builder(context)
        .allowHardware(false)
        .data(imageUrl)
        .target { result ->
            originalImageBitmap = result.current.toBitmap().asImageBitmap()
        }
        .build()

    LaunchedEffect(request) {
        withContext(Dispatchers.IO) {
            imageLoader.execute(request)
        }
    }

    Box(
        modifier = modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTransformGestures { _, pan, zoom, rotationChange ->
                    // Zoom
                    scale *= zoom
                    scale = scale.coerceIn(minScale, maxScale)
                    // Pan (move)
                    offset = if (scale > minScale) {
                        val offsetX = (offset.x + pan.x * scale).coerceIn(
                            -maxWidthVal * (scale - 1),
                            maxWidthVal * (scale - 1)
                        )
                        val offsetY = (offset.y + pan.y * scale).coerceIn(
                            -maxHeightVal * (scale - 1),
                            maxHeightVal * (scale - 1)
                        )
                        Log.d("CropImage", "ZoomableImage: move $offsetX, $offsetY scale $scale")
                        Offset(offsetX, offsetY)
                    } else {
                        Offset(0f, 0f)
                    }
                    // Rotation cap to 360 degrees, cause if we spin more its big than 360
                    // make canvas fail to build up so we mod by 360
                    rotation = (rotation + rotationChange) % 360
                    Log.d("CropImage", "ZoomableImage: rotation $rotation")
                    croppedImage = null
                }
            }
            .background(MaterialTheme.colorScheme.onSurface),
        contentAlignment = Alignment.Center
    ) {
        if (croppedImage == null) {
            if (originalImageBitmap == null) return
            Canvas(
                modifier = Modifier
                    .fillMaxSize()
                    .onGloballyPositioned {
                        canvasSize = Size(it.size.width.toFloat(), it.size.height.toFloat())
                    }
                    .clickable {
                        croppedImage = getCroppedBitmap(
                            originalImageBitmap!!.asAndroidBitmap(),
                            scale,
                            scale,
                            offset.x,
                            offset.y,
                            rotation,
                            canvasSize
                        )
                    },
                onDraw = {
                    val circlePath = Path().apply {
                        addOval(Rect(center, radius = size.width / 2))
                    }

                    val centerX = center.x - ((originalImageBitmap!!.width) / 2) + offset.x // calculate to make photo center
                    val centerY = center.y - ((originalImageBitmap!!.height) /  2) + offset.y // calculate to make photo center

                    rotate(rotation, Offset(center.x, center.y)) {
                        scale(scale, Offset(center.x, center.y)) {
                            drawImage(originalImageBitmap!!, topLeft = Offset(centerX, centerY))
                        }
                    }

                    // Draw the white border
                    val borderPaint = Paint().asFrameworkPaint()
                    borderPaint.color = Color.White.toArgb()
                    // borderPaint.style = Paint().style.Stroke
                    borderPaint.strokeWidth = 4f // Adjust the width as needed

                    drawPath(
                        circlePath,
                        color = Color.White,
                        colorFilter = ColorFilter.tint(Color.White),
                        style = Stroke(4f)
                    )

                    clipPath(circlePath, clipOp = ClipOp.Difference) {
                        drawRect(SolidColor(Color.Black.copy(alpha = 0.8f)))
                    }
                })
        }
    }

    croppedImage?.let {
        Image(
            painter = rememberAsyncImagePainter(it),
            contentDescription = null,
            modifier = Modifier
                .fillMaxSize()
                .background(color = Color.Blue)
                .clickable { croppedImage = null },
        )
    }
}

此功能用于根据图像的位置将图像裁剪为圆圈

fun getCroppedBitmap(
    bitmap: Bitmap,
    scaleX: Float,
    scaleY: Float,
    translationX: Float,
    translationY: Float,
    rotationZ: Float,
    canvasSize: Size,
): Bitmap {
    Log.d("CropImage", "ZoomableImage: scale: $scaleX, translationX: $translationX, translationY: $translationY, rotationZ: $rotationZ, canvasSize: $canvasSize")
    val center = Offset(canvasSize.width.toFloat() / 2, canvasSize.height.toFloat() / 2)
    val circle = Rect(center = Offset(0f, 0f), radius = 500f)
    val output = Bitmap.createBitmap(
        canvasSize.width.toInt(),
        canvasSize.height.toInt(),
        Bitmap.Config.ARGB_8888
    )
    val canvas = Canvas(output.asImageBitmap())
    val color = -0xbdbdbe
    val paint = Paint()
    paint.isAntiAlias = true
    paint.color = Color(color = color)
    val radius = canvasSize.width / 2
    val scaleValue = canvasSize.height / bitmap.height
    Log.d("CropImage", "getCroppedBitmap: original radius ${canvasSize.width / 2} with size $canvasSize, new radius: $radius")
    Log.d("CropImage", "getCroppedBitmap: scaleValue: $scaleValue")
    Log.d("CropImage", "getCroppedBitmap: translationX: ${translationX / scaleValue}, translationY: ${translationY / scaleValue}")
    canvas.drawCircle(
        center = center, radius = radius,
        paint = paint
    )


    canvas.save()
    canvas.rotate(rotationZ, center.x, center.y)
    canvas.scale(scaleX, scaleY, center.x, center.y)
    val imageBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true).asImageBitmap()
    var offsetX = center.x - ((bitmap.width) /  2) // calculate to make photo center
    var offsetY = center.y - ((bitmap.height) /  2 ) // calculate to make photo center
    offsetX += (translationX)
    offsetY += (translationY)

    paint.blendMode = BlendMode.SrcIn
    Log.d("CropImage", "getCroppedBitmap: offsetX: $offsetX offsetY: $offsetY")
    canvas.save()
    canvas.drawImage(imageBitmap, Offset(offsetX, offsetY), paint)


    canvas.restore()
    return output
}

这是如何调用视图

ZoomableImage(
     imageUrl = "https://picsum.photos/seed/picsum/2000/1500".toUri(),
     maxWidthVal = 2000,
     maxHeightVal = 1500
)

这是我的数学错误还是我做错了,如果你建议使用 lib,我们不能这样做,因为我们计划构建一些不同的东西,而这是一个需要处理的基本功能

此致敬意

安卓 android-jetpack-compose android-canvas android-compose

评论


答: 暂无答案