Add free adjustment of image position when selecting background
- Updated AGP version to 8.9.2. - Added support for Android 16 (36). - Replaced the new API and fixed some minor bugs. Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
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.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
|
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageEditorDialog(
|
||||||
|
imageUri: Uri,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (Uri) -> Unit
|
||||||
|
) {
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = true,
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
usePlatformDefaultWidth = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.9f))
|
||||||
|
) {
|
||||||
|
// 主图片区域
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(imageUri)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = stringResource(R.string.settings_custom_background),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer(
|
||||||
|
scaleX = scale,
|
||||||
|
scaleY = scale,
|
||||||
|
translationX = offsetX,
|
||||||
|
translationY = offsetY
|
||||||
|
)
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 顶部工具栏
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.cancel),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||||
|
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||||
|
savedUri?.let { onConfirm(it) }
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(R.string.confirm),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部提示
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.image_editor_hint),
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ import com.topjohnwu.superuser.Shell
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import com.sukisu.ultra.ui.component.ImageEditorDialog
|
||||||
import com.sukisu.ultra.ui.component.SwitchItem
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
import com.sukisu.ultra.ui.theme.*
|
import com.sukisu.ultra.ui.theme.*
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
@@ -140,6 +141,10 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
|||||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 图片编辑状态
|
||||||
|
var showImageEditor by remember { mutableStateOf(false) }
|
||||||
|
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
// 初始化卡片配置
|
// 初始化卡片配置
|
||||||
val systemIsDark = isSystemInDarkTheme()
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
@@ -183,11 +188,29 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
|||||||
ActivityResultContracts.GetContent()
|
ActivityResultContracts.GetContent()
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
context.saveCustomBackground(it)
|
selectedImageUri = it
|
||||||
|
showImageEditor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示图片编辑对话框
|
||||||
|
if (showImageEditor && selectedImageUri != null) {
|
||||||
|
ImageEditorDialog(
|
||||||
|
imageUri = selectedImageUri!!,
|
||||||
|
onDismiss = {
|
||||||
|
showImageEditor = false
|
||||||
|
selectedImageUri = null
|
||||||
|
},
|
||||||
|
onConfirm = { transformedUri ->
|
||||||
|
context.saveAndApplyCustomBackground(transformedUri)
|
||||||
isCustomBackgroundEnabled = true
|
isCustomBackgroundEnabled = true
|
||||||
CardConfig.cardElevation = 0.dp
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
saveCardConfig(context)
|
saveCardConfig(context)
|
||||||
|
showImageEditor = false
|
||||||
|
selectedImageUri = null
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -439,6 +462,7 @@ fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
|||||||
CardConfig.cardElevation = CardConfig.defaultElevation
|
CardConfig.cardElevation = CardConfig.defaultElevation
|
||||||
CardConfig.cardAlpha = 0.45f
|
CardConfig.cardAlpha = 0.45f
|
||||||
CardConfig.isCustomAlphaSet = false
|
CardConfig.isCustomAlphaSet = false
|
||||||
|
CardConfig.isCustomBackgroundEnabled = false
|
||||||
saveCardConfig(context)
|
saveCardConfig(context)
|
||||||
cardAlpha = 0.35f
|
cardAlpha = 0.35f
|
||||||
themeMode = 0
|
themeMode = 0
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -44,11 +46,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
@@ -99,8 +100,8 @@ fun AppProfileTemplateScreen(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val clipboardManager = context.getSystemService<ClipboardManager>()
|
||||||
val showToast = fun(msg: String) {
|
val showToast = fun(msg: String) {
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||||
@@ -112,21 +113,21 @@ fun AppProfileTemplateScreen(
|
|||||||
scope.launch { viewModel.fetchTemplates(true) }
|
scope.launch { viewModel.fetchTemplates(true) }
|
||||||
},
|
},
|
||||||
onImport = {
|
onImport = {
|
||||||
clipboardManager.getText()?.text?.let {
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString()
|
||||||
|
if (clipboardText.isNullOrEmpty()) {
|
||||||
|
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
viewModel.importTemplates(
|
viewModel.importTemplates(
|
||||||
it, {
|
clipboardText,
|
||||||
|
{
|
||||||
showToast(context.getString(R.string.app_profile_template_import_success))
|
showToast(context.getString(R.string.app_profile_template_import_success))
|
||||||
viewModel.fetchTemplates(false)
|
viewModel.fetchTemplates(false)
|
||||||
},
|
},
|
||||||
showToast
|
showToast
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onExport = {
|
onExport = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -134,8 +135,8 @@ fun AppProfileTemplateScreen(
|
|||||||
{
|
{
|
||||||
showToast(context.getString(R.string.app_profile_template_export_empty))
|
showToast(context.getString(R.string.app_profile_template_export_empty))
|
||||||
}
|
}
|
||||||
) {
|
) { text ->
|
||||||
clipboardManager.setText(AnnotatedString(it))
|
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,11 +27,14 @@ import androidx.compose.ui.zIndex
|
|||||||
import coil.compose.rememberAsyncImagePainter
|
import coil.compose.rememberAsyncImagePainter
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
|
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||||
|
|
||||||
object ThemeConfig {
|
object ThemeConfig {
|
||||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||||
@@ -88,29 +91,6 @@ private fun getLightColorScheme() = lightColorScheme(
|
|||||||
outlineVariant = Color.Black.copy(alpha = 0.12f)
|
outlineVariant = Color.Black.copy(alpha = 0.12f)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 复制图片到应用内部存储
|
|
||||||
fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
|
||||||
try {
|
|
||||||
val contentResolver: ContentResolver = contentResolver
|
|
||||||
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
|
|
||||||
val fileName = "custom_background.jpg"
|
|
||||||
val file = File(filesDir, fileName)
|
|
||||||
val outputStream = FileOutputStream(file)
|
|
||||||
val buffer = ByteArray(4 * 1024)
|
|
||||||
var read: Int
|
|
||||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
|
||||||
outputStream.write(buffer, 0, read)
|
|
||||||
}
|
|
||||||
outputStream.flush()
|
|
||||||
outputStream.close()
|
|
||||||
inputStream.close()
|
|
||||||
return Uri.fromFile(file)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KernelSUTheme(
|
fun KernelSUTheme(
|
||||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||||
@@ -131,7 +111,6 @@ fun KernelSUTheme(
|
|||||||
if (darkTheme) {
|
if (darkTheme) {
|
||||||
val originalScheme = dynamicDarkColorScheme(context)
|
val originalScheme = dynamicDarkColorScheme(context)
|
||||||
originalScheme.copy(
|
originalScheme.copy(
|
||||||
// 调整按钮相关颜色
|
|
||||||
primary = adjustColor(originalScheme.primary),
|
primary = adjustColor(originalScheme.primary),
|
||||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
onPrimary = adjustColor(originalScheme.onPrimary),
|
||||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
||||||
@@ -242,6 +221,48 @@ fun KernelSUTheme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制图片到应用内部存储
|
||||||
|
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||||
|
try {
|
||||||
|
val contentResolver: ContentResolver = contentResolver
|
||||||
|
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
|
||||||
|
val fileName = "custom_background.jpg"
|
||||||
|
val file = File(filesDir, fileName)
|
||||||
|
val outputStream = FileOutputStream(file)
|
||||||
|
val buffer = ByteArray(4 * 1024)
|
||||||
|
var read: Int
|
||||||
|
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
inputStream.close()
|
||||||
|
return Uri.fromFile(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存变换后的背景图片到应用内部存储并更新配置
|
||||||
|
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
||||||
|
val finalUri = if (transformation != null) {
|
||||||
|
saveTransformedBackground(uri, transformation)
|
||||||
|
} else {
|
||||||
|
copyImageToInternalStorage(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString("custom_background", finalUri?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.customBackgroundUri = finalUri
|
||||||
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存背景图片到应用内部存储并更新配置
|
||||||
fun Context.saveCustomBackground(uri: Uri?) {
|
fun Context.saveCustomBackground(uri: Uri?) {
|
||||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
@@ -249,6 +270,10 @@ fun Context.saveCustomBackground(uri: Uri?) {
|
|||||||
putString("custom_background", newUri?.toString())
|
putString("custom_background", newUri?.toString())
|
||||||
}
|
}
|
||||||
ThemeConfig.customBackgroundUri = newUri
|
ThemeConfig.customBackgroundUri = newUri
|
||||||
|
if (uri != null) {
|
||||||
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.loadCustomBackground() {
|
fun Context.loadCustomBackground() {
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
data class BackgroundTransformation(
|
||||||
|
val scale: Float = 1f,
|
||||||
|
val offsetX: Float = 0f,
|
||||||
|
val offsetY: Float = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Context.getImageBitmap(uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val contentResolver: ContentResolver = contentResolver
|
||||||
|
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream.close()
|
||||||
|
bitmap
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap {
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
|
||||||
|
// 创建与屏幕比例相同的目标位图
|
||||||
|
val displayMetrics = resources.displayMetrics
|
||||||
|
val screenWidth = displayMetrics.widthPixels
|
||||||
|
val screenHeight = displayMetrics.heightPixels
|
||||||
|
val screenRatio = screenHeight.toFloat() / screenWidth.toFloat()
|
||||||
|
|
||||||
|
// 计算目标宽高
|
||||||
|
val targetWidth: Int
|
||||||
|
val targetHeight: Int
|
||||||
|
if (width.toFloat() / height.toFloat() > screenRatio) {
|
||||||
|
targetHeight = height
|
||||||
|
targetWidth = (height / screenRatio).toInt()
|
||||||
|
} else {
|
||||||
|
targetWidth = width
|
||||||
|
targetHeight = (width * screenRatio).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建与目标相同大小的位图
|
||||||
|
val scaledBitmap = createBitmap(targetWidth, targetHeight)
|
||||||
|
val canvas = Canvas(scaledBitmap)
|
||||||
|
|
||||||
|
val matrix = Matrix()
|
||||||
|
|
||||||
|
matrix.postScale(transformation.scale, transformation.scale)
|
||||||
|
|
||||||
|
// 计算中心点
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 将原始位图绘制到新位图上
|
||||||
|
canvas.drawBitmap(bitmap, matrix, null)
|
||||||
|
|
||||||
|
return scaledBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? {
|
||||||
|
try {
|
||||||
|
val bitmap = getImageBitmap(uri) ?: return null
|
||||||
|
val transformedBitmap = applyTransformationToBitmap(bitmap, transformation)
|
||||||
|
|
||||||
|
val fileName = "custom_background_transformed.jpg"
|
||||||
|
val file = File(filesDir, fileName)
|
||||||
|
val outputStream = FileOutputStream(file)
|
||||||
|
|
||||||
|
transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
|
||||||
|
return Uri.fromFile(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -173,7 +173,6 @@
|
|||||||
<string name="restore_allowlist">許可リストを復元</string>
|
<string name="restore_allowlist">許可リストを復元</string>
|
||||||
<string name="settings_custom_background">カスタム背景を設定</string>
|
<string name="settings_custom_background">カスタム背景を設定</string>
|
||||||
<string name="settings_custom_background_summary">カスタム背景を設定します</string>
|
<string name="settings_custom_background_summary">カスタム背景を設定します</string>
|
||||||
<string name="settings_card_manage">カードの管理</string>
|
|
||||||
<string name="settings_card_alpha">ナビゲーションバーの透過</string>
|
<string name="settings_card_alpha">ナビゲーションバーの透過</string>
|
||||||
<string name="settings_restore_default">デフォルトに復元</string>
|
<string name="settings_restore_default">デフォルトに復元</string>
|
||||||
<string name="home_android_version">Android のバージョン</string>
|
<string name="home_android_version">Android のバージョン</string>
|
||||||
|
|||||||
@@ -262,4 +262,7 @@
|
|||||||
<string name="invalid_file_type">文件类型不正确,请选择 .kpm 文件</string>
|
<string name="invalid_file_type">文件类型不正确,请选择 .kpm 文件</string>
|
||||||
<string name="confirm_uninstall_title_with_filename">卸载</string>
|
<string name="confirm_uninstall_title_with_filename">卸载</string>
|
||||||
<string name="confirm_uninstall_content">将卸载以下 kpm 模块:\n%s</string>
|
<string name="confirm_uninstall_content">将卸载以下 kpm 模块:\n%s</string>
|
||||||
|
<string name="image_editor_title">调整背景图片</string>
|
||||||
|
<string name="image_editor_hint">使用双指缩放图片,单指拖动调整位置</string>
|
||||||
|
<string name="background_image_error">无法加载图片</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -266,4 +266,7 @@
|
|||||||
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
|
<string name="confirm_uninstall_title_with_filename">Uninstall</string>
|
||||||
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
|
<string name="confirm_uninstall_content">The following KPM will be uninstalled: %s</string>
|
||||||
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</string>
|
<string name="settings_susfs_toggle_summary">Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method.</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="background_image_error">Could not load image</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ cmaker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val androidMinSdkVersion = 26
|
val androidMinSdkVersion = 26
|
||||||
val androidTargetSdkVersion = 35
|
val androidTargetSdkVersion = 36
|
||||||
val androidCompileSdkVersion = 35
|
val androidCompileSdkVersion = 36
|
||||||
val androidCompileNdkVersion = "28.0.13004108"
|
val androidCompileNdkVersion = "28.0.13004108"
|
||||||
val androidSourceCompatibility = JavaVersion.VERSION_21
|
val androidSourceCompatibility = JavaVersion.VERSION_21
|
||||||
val androidTargetCompatibility = JavaVersion.VERSION_21
|
val androidTargetCompatibility = JavaVersion.VERSION_21
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.9.1"
|
agp = "8.9.2"
|
||||||
kotlin = "2.1.10"
|
kotlin = "2.1.10"
|
||||||
ksp = "2.1.10-1.0.30"
|
ksp = "2.1.10-1.0.30"
|
||||||
compose-bom = "2025.02.00"
|
compose-bom = "2025.04.01"
|
||||||
lifecycle = "2.8.7"
|
lifecycle = "2.8.7"
|
||||||
navigation = "2.8.7"
|
navigation = "2.8.9"
|
||||||
activity-compose = "1.10.0"
|
activity-compose = "1.10.1"
|
||||||
kotlinx-coroutines = "1.10.1"
|
kotlinx-coroutines = "1.10.2"
|
||||||
coil-compose = "2.7.0"
|
coil-compose = "2.7.0"
|
||||||
compose-destination = "2.1.0-beta16"
|
compose-destination = "2.1.0"
|
||||||
sheets-compose-dialogs = "1.3.0"
|
sheets-compose-dialogs = "1.3.0"
|
||||||
markdown = "4.6.2"
|
markdown = "4.6.2"
|
||||||
webkit = "1.12.1"
|
webkit = "1.13.0"
|
||||||
appiconloader-coil = "1.5.0"
|
appiconloader-coil = "1.5.0"
|
||||||
parcelablelist = "2.0.1"
|
parcelablelist = "2.0.1"
|
||||||
libsu = "6.0.0"
|
libsu = "6.0.0"
|
||||||
apksign = "1.4"
|
apksign = "1.4"
|
||||||
cmaker = "1.2"
|
cmaker = "1.2"
|
||||||
compose-material = "1.7.8"
|
compose-material = "1.8.0"
|
||||||
compose-material3 = "1.3.1"
|
compose-material3 = "1.3.2"
|
||||||
compose-ui = "1.7.8"
|
compose-ui = "1.8.0"
|
||||||
compose-foundation = "1.7.8"
|
compose-foundation = "1.7.8"
|
||||||
documentfile = "1.0.1"
|
documentfile = "1.0.1"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user