From 7769a23f59070b9b245e9ce35d4c7c57347f81e2 Mon Sep 17 00:00:00 2001
From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Date: Sun, 27 Apr 2025 14:02:16 +0800
Subject: [PATCH] Opt the image editing dialog box, add full-screen zoom
function, improve the panning limit to ensure the security of image
transformation.
---
.../ultra/ui/component/ImageEditorDialog.kt | 120 +++++++++++++++---
.../sukisu/ultra/ui/util/BackgroundUtils.kt | 30 ++++-
.../src/main/res/values-zh-rCN/strings.xml | 1 +
manager/app/src/main/res/values/strings.xml | 1 +
4 files changed, 127 insertions(+), 25 deletions(-)
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt
index 0bb280cc..d4e4b074 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt
@@ -1,6 +1,7 @@
package com.sukisu.ultra.ui.component
import android.net.Uri
+import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.*
@@ -8,6 +9,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -18,6 +20,7 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -27,6 +30,12 @@ import coil.request.ImageRequest
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.BackgroundTransformation
import com.sukisu.ultra.ui.util.saveTransformedBackground
+import kotlinx.coroutines.launch
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.layout.onSizeChanged
+import kotlin.math.max
@Composable
fun ImageEditorDialog(
@@ -38,6 +47,48 @@ fun ImageEditorDialog(
var offsetX by remember { mutableFloatStateOf(0f) }
var offsetY by remember { mutableFloatStateOf(0f) }
val context = LocalContext.current
+ val scope = rememberCoroutineScope()
+ val density = LocalDensity.current
+ var lastScale by remember { mutableFloatStateOf(1f) }
+ var lastOffsetX by remember { mutableFloatStateOf(0f) }
+ var lastOffsetY by remember { mutableFloatStateOf(0f) }
+ var imageSize by remember { mutableStateOf(Size.Zero) }
+ var screenSize by remember { mutableStateOf(Size.Zero) }
+ val animatedScale by animateFloatAsState(
+ targetValue = scale,
+ label = "ScaleAnimation"
+ )
+ val animatedOffsetX by animateFloatAsState(
+ targetValue = offsetX,
+ label = "OffsetXAnimation"
+ )
+ val animatedOffsetY by animateFloatAsState(
+ targetValue = offsetY,
+ label = "OffsetYAnimation"
+ )
+ val updateTransformation = remember {
+ { newScale: Float, newOffsetX: Float, newOffsetY: Float ->
+ val scaleDiff = kotlin.math.abs(newScale - lastScale)
+ val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
+ val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
+ if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
+ scale = newScale
+ offsetX = newOffsetX
+ offsetY = newOffsetY
+ lastScale = newScale
+ lastOffsetX = newOffsetX
+ lastOffsetY = newOffsetY
+ }
+ }
+ }
+ val scaleToFullScreen = remember {
+ {
+ if (imageSize.height > 0 && screenSize.height > 0) {
+ val newScale = screenSize.height / imageSize.height
+ updateTransformation(newScale, 0f, 0f)
+ }
+ }
+ }
Dialog(
onDismissRequest = onDismiss,
@@ -51,8 +102,10 @@ fun ImageEditorDialog(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.9f))
+ .onSizeChanged { size ->
+ screenSize = Size(size.width.toFloat(), size.height.toFloat())
+ }
) {
- // 主图片区域
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUri)
@@ -63,24 +116,39 @@ fun ImageEditorDialog(
modifier = Modifier
.fillMaxSize()
.graphicsLayer(
- scaleX = scale,
- scaleY = scale,
- translationX = offsetX,
- translationY = offsetY
+ scaleX = animatedScale,
+ scaleY = animatedScale,
+ translationX = animatedOffsetX,
+ translationY = animatedOffsetY
)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
- scale = (scale * zoom).coerceIn(0.5f, 3f)
-
- // 限制平移范围,防止图片完全移出屏幕
- val maxOffset = size.width * (scale - 1) / 2
- offsetX = (offsetX + pan.x).coerceIn(-maxOffset, maxOffset)
- offsetY = (offsetY + pan.y).coerceIn(-maxOffset, maxOffset)
+ scope.launch {
+ try {
+ val newScale = (scale * zoom).coerceIn(0.5f, 3f)
+ val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
+ val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
+ val newOffsetX = if (maxOffsetX > 0) {
+ (offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
+ } else {
+ 0f
+ }
+ val newOffsetY = if (maxOffsetY > 0) {
+ (offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
+ } else {
+ 0f
+ }
+ updateTransformation(newScale, newOffsetX, newOffsetY)
+ } catch (e: Exception) {
+ updateTransformation(lastScale, lastOffsetX, lastOffsetY)
+ }
+ }
}
}
+ .onSizeChanged { size ->
+ imageSize = Size(size.width.toFloat(), size.height.toFloat())
+ }
)
-
- // 顶部工具栏
Row(
modifier = Modifier
.fillMaxWidth()
@@ -100,12 +168,29 @@ fun ImageEditorDialog(
tint = Color.White
)
}
-
+ IconButton(
+ onClick = { scaleToFullScreen() },
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .background(Color.Black.copy(alpha = 0.6f))
+ ) {
+ Icon(
+ imageVector = Icons.Default.Fullscreen,
+ contentDescription = stringResource(R.string.reprovision),
+ tint = Color.White
+ )
+ }
IconButton(
onClick = {
- val transformation = BackgroundTransformation(scale, offsetX, offsetY)
- val savedUri = context.saveTransformedBackground(imageUri, transformation)
- savedUri?.let { onConfirm(it) }
+ scope.launch {
+ try {
+ val transformation = BackgroundTransformation(scale, offsetX, offsetY)
+ val savedUri = context.saveTransformedBackground(imageUri, transformation)
+ savedUri?.let { onConfirm(it) }
+ } catch (e: Exception) {
+ ""
+ }
+ }
},
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
@@ -119,7 +204,6 @@ fun ImageEditorDialog(
}
}
- // 底部提示
Box(
modifier = Modifier
.fillMaxWidth()
diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt
index 8bbcd1de..481e6dfa 100644
--- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt
+++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt
@@ -59,17 +59,33 @@ fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: Backgrou
val matrix = Matrix()
- matrix.postScale(transformation.scale, transformation.scale)
+ // 确保缩放值有效
+ val safeScale = maxOf(0.1f, transformation.scale)
+ matrix.postScale(safeScale, safeScale)
// 计算中心点
val centerX = targetWidth / 2f
val centerY = targetHeight / 2f
- // 缩放围绕中心点
- matrix.postTranslate(
- -((bitmap.width * transformation.scale - targetWidth) / 2) + transformation.offsetX,
- -((bitmap.height * transformation.scale - targetHeight) / 2) + transformation.offsetY
- )
+ // 计算偏移量,确保不会出现负最大值的问题
+ val widthDiff = (bitmap.width * safeScale - targetWidth)
+ val heightDiff = (bitmap.height * safeScale - targetHeight)
+
+ // 安全计算偏移量边界
+ val maxOffsetX = maxOf(0f, widthDiff / 2)
+ val maxOffsetY = maxOf(0f, heightDiff / 2)
+
+ // 限制偏移范围
+ val safeOffsetX = if (maxOffsetX > 0)
+ transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f
+ val safeOffsetY = if (maxOffsetY > 0)
+ transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f
+
+ // 应用偏移量到矩阵
+ val translationX = -widthDiff / 2 + safeOffsetX
+ val translationY = -heightDiff / 2 + safeOffsetY
+
+ matrix.postTranslate(translationX, translationY)
// 将原始位图绘制到新位图上
canvas.drawBitmap(bitmap, matrix, null)
@@ -92,7 +108,7 @@ fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransf
return Uri.fromFile(file)
} catch (e: Exception) {
- Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}")
+ Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e)
return null
}
}
\ No newline at end of file
diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml
index 509ad3da..7442e2ae 100644
--- a/manager/app/src/main/res/values-zh-rCN/strings.xml
+++ b/manager/app/src/main/res/values-zh-rCN/strings.xml
@@ -265,4 +265,5 @@
调整背景图片
使用双指缩放图片,单指拖动调整位置
无法加载图片
+ 重置
diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml
index 594ce789..894ed0f7 100644
--- a/manager/app/src/main/res/values/strings.xml
+++ b/manager/app/src/main/res/values/strings.xml
@@ -269,4 +269,5 @@
Adjust background image
Use two fingers to zoom the image, and one finger to drag it to adjust the position
Could not load image
+ Reprovision