Opt the image editing dialog box, add full-screen zoom function, improve the panning limit to ensure the security of image transformation.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package com.sukisu.ultra.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
import androidx.compose.foundation.layout.*
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Fullscreen
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
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.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -27,6 +30,12 @@ import coil.request.ImageRequest
|
|||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
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
|
@Composable
|
||||||
fun ImageEditorDialog(
|
fun ImageEditorDialog(
|
||||||
@@ -38,6 +47,48 @@ fun ImageEditorDialog(
|
|||||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
val context = LocalContext.current
|
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(
|
Dialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -51,8 +102,10 @@ fun ImageEditorDialog(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.Black.copy(alpha = 0.9f))
|
.background(Color.Black.copy(alpha = 0.9f))
|
||||||
|
.onSizeChanged { size ->
|
||||||
|
screenSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
// 主图片区域
|
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
.data(imageUri)
|
.data(imageUri)
|
||||||
@@ -63,24 +116,39 @@ fun ImageEditorDialog(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer(
|
.graphicsLayer(
|
||||||
scaleX = scale,
|
scaleX = animatedScale,
|
||||||
scaleY = scale,
|
scaleY = animatedScale,
|
||||||
translationX = offsetX,
|
translationX = animatedOffsetX,
|
||||||
translationY = offsetY
|
translationY = animatedOffsetY
|
||||||
)
|
)
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTransformGestures { _, pan, zoom, _ ->
|
detectTransformGestures { _, pan, zoom, _ ->
|
||||||
scale = (scale * zoom).coerceIn(0.5f, 3f)
|
scope.launch {
|
||||||
|
try {
|
||||||
// 限制平移范围,防止图片完全移出屏幕
|
val newScale = (scale * zoom).coerceIn(0.5f, 3f)
|
||||||
val maxOffset = size.width * (scale - 1) / 2
|
val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
|
||||||
offsetX = (offsetX + pan.x).coerceIn(-maxOffset, maxOffset)
|
val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
|
||||||
offsetY = (offsetY + pan.y).coerceIn(-maxOffset, maxOffset)
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -100,12 +168,29 @@ fun ImageEditorDialog(
|
|||||||
tint = Color.White
|
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(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
scope.launch {
|
||||||
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
try {
|
||||||
savedUri?.let { onConfirm(it) }
|
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||||
|
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||||
|
savedUri?.let { onConfirm(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
@@ -119,7 +204,6 @@ fun ImageEditorDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 底部提示
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -59,17 +59,33 @@ fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: Backgrou
|
|||||||
|
|
||||||
val matrix = Matrix()
|
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 centerX = targetWidth / 2f
|
||||||
val centerY = targetHeight / 2f
|
val centerY = targetHeight / 2f
|
||||||
|
|
||||||
// 缩放围绕中心点
|
// 计算偏移量,确保不会出现负最大值的问题
|
||||||
matrix.postTranslate(
|
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
||||||
-((bitmap.width * transformation.scale - targetWidth) / 2) + transformation.offsetX,
|
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
||||||
-((bitmap.height * transformation.scale - targetHeight) / 2) + transformation.offsetY
|
|
||||||
)
|
// 安全计算偏移量边界
|
||||||
|
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)
|
canvas.drawBitmap(bitmap, matrix, null)
|
||||||
@@ -92,7 +108,7 @@ fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransf
|
|||||||
|
|
||||||
return Uri.fromFile(file)
|
return Uri.fromFile(file)
|
||||||
} catch (e: Exception) {
|
} 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
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,4 +265,5 @@
|
|||||||
<string name="image_editor_title">调整背景图片</string>
|
<string name="image_editor_title">调整背景图片</string>
|
||||||
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
|
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
|
||||||
<string name="background_image_error">无法加载图片</string>
|
<string name="background_image_error">无法加载图片</string>
|
||||||
|
<string name="reprovision">重置</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -269,4 +269,5 @@
|
|||||||
<string name="image_editor_title">Adjust background image</string>
|
<string name="image_editor_title">Adjust background image</string>
|
||||||
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
<string name="image_editor_hint">Use two fingers to zoom the image, and one finger to drag it to adjust the position</string>
|
||||||
<string name="background_image_error">Could not load image</string>
|
<string name="background_image_error">Could not load image</string>
|
||||||
|
<string name="reprovision">Reprovision</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user