manager: Rewrite UI state management

This commit is contained in:
ShirkNeko
2025-10-18 22:37:17 +08:00
parent 7b6470cc79
commit 776a753206
4 changed files with 881 additions and 636 deletions

View File

@@ -6,114 +6,187 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Stable
object CardConfig { object CardConfig {
// 卡片透明度 // 卡片透明度
var cardAlpha by mutableFloatStateOf(1f) var cardAlpha by mutableFloatStateOf(1f)
internal set
// 卡片亮度 // 卡片亮度
var cardDim by mutableFloatStateOf(0f) var cardDim by mutableFloatStateOf(0f)
internal set
// 卡片阴影 // 卡片阴影
var cardElevation by mutableStateOf(0.dp) var cardElevation by mutableStateOf(0.dp)
var isShadowEnabled by mutableStateOf(true) internal set
var isCustomAlphaSet by mutableStateOf(false)
var isCustomDimSet by mutableStateOf(false) // 功能开关
var isUserDarkModeEnabled by mutableStateOf(false) var isShadowEnabled by mutableStateOf(true)
var isUserLightModeEnabled by mutableStateOf(false) internal set
var isCustomBackgroundEnabled by mutableStateOf(false) var isCustomBackgroundEnabled by mutableStateOf(false)
internal set
var isCustomAlphaSet by mutableStateOf(false)
internal set
var isCustomDimSet by mutableStateOf(false)
internal set
var isUserDarkModeEnabled by mutableStateOf(false)
internal set
var isUserLightModeEnabled by mutableStateOf(false)
internal set
// 配置键名
private object Keys {
const val CARD_ALPHA = "card_alpha"
const val CARD_DIM = "card_dim"
const val CUSTOM_BACKGROUND_ENABLED = "custom_background_enabled"
const val IS_SHADOW_ENABLED = "is_shadow_enabled"
const val IS_CUSTOM_ALPHA_SET = "is_custom_alpha_set"
const val IS_CUSTOM_DIM_SET = "is_custom_dim_set"
const val IS_USER_DARK_MODE_ENABLED = "is_user_dark_mode_enabled"
const val IS_USER_LIGHT_MODE_ENABLED = "is_user_light_mode_enabled"
}
fun updateAlpha(alpha: Float, isCustom: Boolean = true) {
cardAlpha = alpha.coerceIn(0f, 1f)
if (isCustom) isCustomAlphaSet = true
}
fun updateDim(dim: Float, isCustom: Boolean = true) {
cardDim = dim.coerceIn(0f, 1f)
if (isCustom) isCustomDimSet = true
}
fun updateShadow(enabled: Boolean, elevation: Dp = 4.dp) {
isShadowEnabled = enabled
cardElevation = if (enabled) elevation else 0.dp
}
fun updateBackground(enabled: Boolean) {
isCustomBackgroundEnabled = enabled
// 自定义背景时自动禁用阴影以获得更好的视觉效果
if (enabled) {
updateShadow(false)
}
}
fun updateThemePreference(darkMode: Boolean?, lightMode: Boolean?) {
isUserDarkModeEnabled = darkMode ?: false
isUserLightModeEnabled = lightMode ?: false
}
fun reset() {
cardAlpha = 1f
cardDim = 0f
cardElevation = 0.dp
isShadowEnabled = true
isCustomBackgroundEnabled = false
isCustomAlphaSet = false
isCustomDimSet = false
isUserDarkModeEnabled = false
isUserLightModeEnabled = false
}
fun setThemeDefaults(isDarkMode: Boolean) {
if (!isCustomAlphaSet) {
updateAlpha(if (isDarkMode) 0.88f else 1f, false)
}
if (!isCustomDimSet) {
updateDim(if (isDarkMode) 0.25f else 0f, false)
}
// 暗色模式下默认启用轻微阴影
if (isDarkMode && !isCustomBackgroundEnabled) {
updateShadow(true, 2.dp)
}
}
/**
* 保存卡片配置到SharedPreferences
*/
fun save(context: Context) { fun save(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE)
prefs.edit().apply { prefs.edit().apply {
putFloat("card_alpha", cardAlpha) putFloat(Keys.CARD_ALPHA, cardAlpha)
putFloat("card_dim", cardDim) putFloat(Keys.CARD_DIM, cardDim)
putBoolean("custom_background_enabled", isCustomBackgroundEnabled) putBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, isCustomBackgroundEnabled)
putBoolean("is_shadow_enabled", isShadowEnabled) putBoolean(Keys.IS_SHADOW_ENABLED, isShadowEnabled)
putBoolean("is_custom_alpha_set", isCustomAlphaSet) putBoolean(Keys.IS_CUSTOM_ALPHA_SET, isCustomAlphaSet)
putBoolean("is_custom_dim_set", isCustomDimSet) putBoolean(Keys.IS_CUSTOM_DIM_SET, isCustomDimSet)
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled) putBoolean(Keys.IS_USER_DARK_MODE_ENABLED, isUserDarkModeEnabled)
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled) putBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, isUserLightModeEnabled)
apply() apply()
} }
} }
/**
* 从SharedPreferences加载卡片配置
*/
fun load(context: Context) { fun load(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE)
cardAlpha = prefs.getFloat("card_alpha", 1f) cardAlpha = prefs.getFloat(Keys.CARD_ALPHA, 1f).coerceIn(0f, 1f)
cardDim = prefs.getFloat("card_dim", 0f) cardDim = prefs.getFloat(Keys.CARD_DIM, 0f).coerceIn(0f, 1f)
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false) isCustomBackgroundEnabled = prefs.getBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, false)
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true) isShadowEnabled = prefs.getBoolean(Keys.IS_SHADOW_ENABLED, true)
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false) isCustomAlphaSet = prefs.getBoolean(Keys.IS_CUSTOM_ALPHA_SET, false)
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false) isCustomDimSet = prefs.getBoolean(Keys.IS_CUSTOM_DIM_SET, false)
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false) isUserDarkModeEnabled = prefs.getBoolean(Keys.IS_USER_DARK_MODE_ENABLED, false)
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false) isUserLightModeEnabled = prefs.getBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, false)
updateShadowEnabled(isShadowEnabled)
// 应用阴影设置
updateShadow(isShadowEnabled, if (isShadowEnabled) 4.dp else 0.dp)
} }
/** @Deprecated("使用 updateShadow 替代", ReplaceWith("updateShadow(enabled)"))
* 更新阴影启用状态
*/
fun updateShadowEnabled(enabled: Boolean) { fun updateShadowEnabled(enabled: Boolean) {
isShadowEnabled = enabled updateShadow(enabled)
cardElevation = 0.dp
}
/**
* 设置主题模式默认值
*/
fun setThemeDefaults(isDarkMode: Boolean) {
if (!isCustomAlphaSet) {
cardAlpha = 1f
}
if (!isCustomDimSet) {
cardDim = if (isDarkMode) 0.5f else 0f
}
updateShadowEnabled(isShadowEnabled)
} }
} }
/** object CardStyleProvider {
* 获取卡片颜色配置
*/
@Composable
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
contentColor = determineContentColor(originalColor)
)
/** @Composable
* 获取卡片阴影配置 fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
*/ containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
@Composable contentColor = determineContentColor(originalColor),
fun getCardElevation() = CardDefaults.cardElevation( disabledContainerColor = originalColor.copy(alpha = CardConfig.cardAlpha * 0.38f),
defaultElevation = CardConfig.cardElevation, disabledContentColor = determineContentColor(originalColor).copy(alpha = 0.38f)
pressedElevation = CardConfig.cardElevation, )
focusedElevation = CardConfig.cardElevation,
hoveredElevation = CardConfig.cardElevation,
draggedElevation = CardConfig.cardElevation,
disabledElevation = CardConfig.cardElevation
)
/** @Composable
* 根据背景颜色、主题模式和用户设置确定内容颜色 fun getCardElevation() = CardDefaults.cardElevation(
*/ defaultElevation = CardConfig.cardElevation,
@Composable pressedElevation = if (CardConfig.isShadowEnabled) {
private fun determineContentColor(originalColor: Color): Color { (CardConfig.cardElevation.value + 4).dp
val isDarkTheme = isSystemInDarkTheme() } else 0.dp,
if (ThemeConfig.isThemeChanging) { focusedElevation = if (CardConfig.isShadowEnabled) {
return if (isDarkTheme) Color.White else Color.Black (CardConfig.cardElevation.value + 6).dp
} } else 0.dp,
hoveredElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 2).dp
} else 0.dp,
draggedElevation = if (CardConfig.isShadowEnabled) {
(CardConfig.cardElevation.value + 8).dp
} else 0.dp,
disabledElevation = 0.dp
)
return when { @Composable
CardConfig.isUserLightModeEnabled -> Color.Black private fun determineContentColor(originalColor: Color): Color {
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black val isDarkTheme = isSystemInDarkTheme()
isDarkTheme -> Color.White
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White return when {
ThemeConfig.isThemeChanging -> {
if (isDarkTheme) Color.White else Color.Black
}
CardConfig.isUserLightModeEnabled -> Color.Black
CardConfig.isUserDarkModeEnabled -> Color.White
else -> {
val luminance = originalColor.luminance()
val threshold = if (isDarkTheme) 0.4f else 0.6f
if (luminance > threshold) Color.Black else Color.White
}
}
} }
} }
// 向后兼容
@Composable
fun getCardColors(originalColor: Color) = CardStyleProvider.getCardColors(originalColor)
@Composable
fun getCardElevation() = CardStyleProvider.getCardElevation()

View File

@@ -1,6 +1,5 @@
package com.sukisu.ultra.ui.theme package com.sukisu.ultra.ui.theme
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@@ -9,9 +8,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.*
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -28,7 +25,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
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.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -36,28 +32,31 @@ import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import com.sukisu.ultra.ui.theme.util.BackgroundTransformation import com.sukisu.ultra.ui.theme.util.BackgroundTransformation
import com.sukisu.ultra.ui.theme.util.saveTransformedBackground import com.sukisu.ultra.ui.theme.util.saveTransformedBackground
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream
/** @Stable
* 主题配置对象,管理应用的主题相关状态
*/
object ThemeConfig { object ThemeConfig {
// 主题状态
var customBackgroundUri by mutableStateOf<Uri?>(null) var customBackgroundUri by mutableStateOf<Uri?>(null)
var forceDarkMode by mutableStateOf<Boolean?>(null) var forceDarkMode by mutableStateOf<Boolean?>(null)
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default) var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
var useDynamicColor by mutableStateOf(false) var useDynamicColor by mutableStateOf(false)
// 背景状态
var backgroundImageLoaded by mutableStateOf(false) var backgroundImageLoaded by mutableStateOf(false)
var needsResetOnThemeChange by mutableStateOf(false)
var isThemeChanging by mutableStateOf(false) var isThemeChanging by mutableStateOf(false)
var preventBackgroundRefresh by mutableStateOf(false) var preventBackgroundRefresh by mutableStateOf(false)
// 主题变化检测
private var lastDarkModeState: Boolean? = null private var lastDarkModeState: Boolean? = null
fun detectThemeChange(currentDarkMode: Boolean): Boolean { fun detectThemeChange(currentDarkMode: Boolean): Boolean {
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode val hasChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
lastDarkModeState = currentDarkMode lastDarkModeState = currentDarkMode
return isChanged return hasChanged
} }
fun resetBackgroundState() { fun resetBackgroundState() {
@@ -66,11 +65,171 @@ object ThemeConfig {
} }
isThemeChanging = true isThemeChanging = true
} }
fun updateTheme(
theme: ThemeColors? = null,
dynamicColor: Boolean? = null,
darkMode: Boolean? = null
) {
theme?.let { currentTheme = it }
dynamicColor?.let { useDynamicColor = it }
darkMode?.let { forceDarkMode = it }
}
fun reset() {
customBackgroundUri = null
forceDarkMode = null
currentTheme = ThemeColors.Default
useDynamicColor = false
backgroundImageLoaded = false
isThemeChanging = false
preventBackgroundRefresh = false
lastDarkModeState = null
}
}
object ThemeManager {
private const val PREFS_NAME = "theme_prefs"
fun saveThemeMode(context: Context, forceDark: Boolean?) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putString("theme_mode", when (forceDark) {
true -> "dark"
false -> "light"
null -> "system"
})
}
ThemeConfig.forceDarkMode = forceDark
}
fun loadThemeMode(context: Context) {
val mode = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when (mode) {
"dark" -> true
"light" -> false
else -> null
}
}
fun saveThemeColors(context: Context, themeName: String) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putString("theme_colors", themeName)
}
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
}
fun loadThemeColors(context: Context) {
val themeName = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getString("theme_colors", "default") ?: "default"
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
}
fun saveDynamicColorState(context: Context, enabled: Boolean) {
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putBoolean("use_dynamic_color", enabled)
}
ThemeConfig.useDynamicColor = enabled
}
fun loadDynamicColorState(context: Context) {
val enabled = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
.getBoolean("use_dynamic_color", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
ThemeConfig.useDynamicColor = enabled
}
}
object BackgroundManager {
private const val TAG = "BackgroundManager"
fun saveAndApplyCustomBackground(
context: Context,
uri: Uri,
transformation: BackgroundTransformation? = null
) {
try {
val finalUri = if (transformation != null) {
context.saveTransformedBackground(uri, transformation)
} else {
copyImageToInternalStorage(context, uri)
}
saveBackgroundUri(context, finalUri)
ThemeConfig.customBackgroundUri = finalUri
CardConfig.updateBackground(true)
resetBackgroundState(context)
} catch (e: Exception) {
Log.e(TAG, "保存背景失败: ${e.message}", e)
}
}
fun clearCustomBackground(context: Context) {
saveBackgroundUri(context, null)
ThemeConfig.customBackgroundUri = null
CardConfig.updateBackground(false)
resetBackgroundState(context)
}
fun loadCustomBackground(context: Context) {
val uriString = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("custom_background", null)
val newUri = uriString?.toUri()
val preventRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", false)
ThemeConfig.preventBackgroundRefresh = preventRefresh
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
Log.d(TAG, "加载自定义背景: $uriString")
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
CardConfig.updateBackground(newUri != null)
}
}
private fun saveBackgroundUri(context: Context, uri: Uri?) {
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putString("custom_background", uri?.toString())
putBoolean("prevent_background_refresh", false)
}
}
private fun resetBackgroundState(context: Context) {
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
}
}
private fun copyImageToInternalStorage(context: Context, uri: Uri): Uri? {
return try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val fileName = "custom_background_${System.currentTimeMillis()}.jpg"
val file = File(context.filesDir, fileName)
FileOutputStream(file).use { outputStream ->
val buffer = ByteArray(8 * 1024)
var read: Int
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
}
outputStream.flush()
}
inputStream.close()
Uri.fromFile(file)
} catch (e: Exception) {
Log.e(TAG, "复制图片失败: ${e.message}", e)
null
}
}
} }
/**
* 应用主题
*/
@Composable @Composable
fun KernelSUTheme( fun KernelSUTheme(
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) { darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
@@ -84,198 +243,223 @@ fun KernelSUTheme(
val context = LocalContext.current val context = LocalContext.current
val systemIsDark = isSystemInDarkTheme() val systemIsDark = isSystemInDarkTheme()
// 检测系统主题变化并保存状态 // 初始化主题
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark) ThemeInitializer(context = context, systemIsDark = systemIsDark)
LaunchedEffect(systemIsDark, themeChanged) {
if (ThemeConfig.forceDarkMode == null && themeChanged) {
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
ThemeConfig.resetBackgroundState()
if (!ThemeConfig.preventBackgroundRefresh) {
context.loadCustomBackground()
}
CardConfig.apply {
load(context)
if (!isCustomAlphaSet) {
cardAlpha = if (systemIsDark) 0.50f else 1f
}
if (!isCustomDimSet) {
cardDim = if (systemIsDark) 0.5f else 0f
}
save(context)
}
}
}
SystemBarStyle(
darkMode = darkTheme
)
// 初始加载配置
LaunchedEffect(Unit) {
context.loadThemeMode()
context.loadThemeColors()
context.loadDynamicColorState()
CardConfig.load(context)
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
context.loadCustomBackground()
ThemeConfig.backgroundImageLoaded = false
}
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", true)
}
// 创建颜色方案 // 创建颜色方案
val colorScheme = when { val colorScheme = createColorScheme(context, darkTheme, dynamicColor)
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
}
darkTheme -> createDarkColorScheme()
else -> createLightColorScheme()
}
// 根据暗色模式和自定义背景调整卡片配置 // 系统栏样式
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null SystemBarController(darkTheme)
if (darkTheme && !dynamicColor) {
CardConfig.setThemeDefaults(true)
} else if (!darkTheme && !dynamicColor) {
CardConfig.setThemeDefaults(false)
}
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
LaunchedEffect(ThemeConfig.customBackgroundUri) {
backgroundUri.value = ThemeConfig.customBackgroundUri
}
val bgImagePainter = backgroundUri.value?.let {
rememberAsyncImagePainter(
model = it,
onError = { err ->
Log.e("ThemeSystem", "背景图加载失败: ${err.result.throwable.message}")
ThemeConfig.customBackgroundUri = null
context.saveCustomBackground(null)
},
onSuccess = {
Log.d("ThemeSystem", "背景图加载成功")
ThemeConfig.backgroundImageLoaded = true
ThemeConfig.isThemeChanging = false
ThemeConfig.preventBackgroundRefresh = true
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit { putBoolean("prevent_background_refresh", true) }
}
)
}
val transition = updateTransition(
targetState = ThemeConfig.backgroundImageLoaded,
label = "bgTransition"
)
val bgAlpha by transition.animateFloat(
label = "bgAlpha",
transitionSpec = {
spring(
dampingRatio = 0.8f,
stiffness = 300f
)
}
) { loaded -> if (loaded) 1f else 0f }
DisposableEffect(systemIsDark) {
onDispose {
if (ThemeConfig.isThemeChanging) {
ThemeConfig.isThemeChanging = false
}
}
}
// 计算适用的暗化值
val dimFactor = CardConfig.cardDim
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography typography = Typography
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Box( // 背景层
modifier = Modifier BackgroundLayer(darkTheme)
.fillMaxSize()
.zIndex(-2f)
.background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }
else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background })
)
// 自定义背景层
backgroundUri.value?.let {
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-1f)
.alpha(bgAlpha)
) {
// 背景图片
bgImagePainter?.let { painter ->
Box(
modifier = Modifier
.fillMaxSize()
.paint(
painter = painter,
contentScale = ContentScale.Crop
)
.graphicsLayer {
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
}
)
}
// 亮度调节层 (根据cardDim调整)
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
)
)
// 边缘渐变遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Transparent,
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
),
radius = 1200f
)
)
)
}
}
// 内容层 // 内容层
Box( Box(modifier = Modifier.fillMaxSize().zIndex(1f)) {
modifier = Modifier
.fillMaxSize()
.zIndex(1f)
) {
content() content()
} }
} }
} }
} }
/** @Composable
* 创建动态深色颜色方案 private fun ThemeInitializer(context: Context, systemIsDark: Boolean) {
*/ val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
val scope = rememberCoroutineScope()
// 处理系统主题变化
LaunchedEffect(systemIsDark, themeChanged) {
if (ThemeConfig.forceDarkMode == null && themeChanged) {
Log.d("ThemeSystem", "系统主题变化: $systemIsDark")
ThemeConfig.resetBackgroundState()
if (!ThemeConfig.preventBackgroundRefresh) {
BackgroundManager.loadCustomBackground(context)
}
CardConfig.apply {
load(context)
setThemeDefaults(systemIsDark)
save(context)
}
}
}
// 初始加载配置
LaunchedEffect(Unit) {
scope.launch {
ThemeManager.loadThemeMode(context)
ThemeManager.loadThemeColors(context)
ThemeManager.loadDynamicColorState(context)
CardConfig.load(context)
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
BackgroundManager.loadCustomBackground(context)
}
}
}
}
@Composable
private fun BackgroundLayer(darkTheme: Boolean) {
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
LaunchedEffect(ThemeConfig.customBackgroundUri) {
backgroundUri.value = ThemeConfig.customBackgroundUri
}
// 默认背景
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-2f)
.background(
if (CardConfig.isCustomBackgroundEnabled) {
MaterialTheme.colorScheme.surfaceContainerLow
} else {
MaterialTheme.colorScheme.background
}
)
)
// 自定义背景
backgroundUri.value?.let { uri ->
CustomBackgroundLayer(uri = uri, darkTheme = darkTheme)
}
}
@Composable
private fun CustomBackgroundLayer(uri: Uri, darkTheme: Boolean) {
val painter = rememberAsyncImagePainter(
model = uri,
onError = { error ->
Log.e("ThemeSystem", "背景加载失败: ${error.result.throwable.message}")
ThemeConfig.customBackgroundUri = null
},
onSuccess = {
Log.d("ThemeSystem", "背景加载成功")
ThemeConfig.backgroundImageLoaded = true
ThemeConfig.isThemeChanging = false
}
)
val transition = updateTransition(
targetState = ThemeConfig.backgroundImageLoaded,
label = "backgroundTransition"
)
val alpha by transition.animateFloat(
label = "backgroundAlpha",
transitionSpec = {
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
}
) { loaded -> if (loaded) 1f else 0f }
Box(
modifier = Modifier
.fillMaxSize()
.zIndex(-1f)
.alpha(alpha)
) {
// 背景图片
Box(
modifier = Modifier
.fillMaxSize()
.paint(painter = painter, contentScale = ContentScale.Crop)
.graphicsLayer {
this.alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
}
)
// 遮罩层
BackgroundOverlay(darkTheme = darkTheme)
}
}
@Composable
private fun BackgroundOverlay(darkTheme: Boolean) {
val dimFactor = CardConfig.cardDim
// 主要遮罩层
Box(
modifier = Modifier
.fillMaxSize()
.background(
if (darkTheme) {
Color.Black.copy(alpha = 0.3f + dimFactor * 0.4f)
} else {
Color.White.copy(alpha = 0.05f + dimFactor * 0.3f)
}
)
)
// 边缘渐变遮罩
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(
Color.Transparent,
if (darkTheme) {
Color.Black.copy(alpha = 0.2f + dimFactor * 0.2f)
} else {
Color.Black.copy(alpha = 0.05f + dimFactor * 0.1f)
}
),
radius = 1000f
)
)
)
}
@Composable
private fun createColorScheme(
context: Context,
darkTheme: Boolean,
dynamicColor: Boolean
): ColorScheme {
return when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) createDynamicDarkColorScheme(context)
else createDynamicLightColorScheme(context)
}
darkTheme -> createDarkColorScheme()
else -> createLightColorScheme()
}
}
@Composable
private fun SystemBarController(darkMode: Boolean) {
val context = LocalContext.current
val activity = context as ComponentActivity
SideEffect {
activity.enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
Color.Transparent.toArgb(),
Color.Transparent.toArgb(),
) { darkMode },
navigationBarStyle = if (darkMode) {
SystemBarStyle.dark(Color.Transparent.toArgb())
} else {
SystemBarStyle.light(
Color.Transparent.toArgb(),
Color.Transparent.toArgb()
)
}
)
}
}
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@Composable @Composable
private fun createDynamicDarkColorScheme(context: Context): ColorScheme { private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
@@ -288,9 +472,6 @@ private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
) )
} }
/**
* 创建动态浅色颜色方案
*/
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@Composable @Composable
private fun createDynamicLightColorScheme(context: Context): ColorScheme { private fun createDynamicLightColorScheme(context: Context): ColorScheme {
@@ -303,11 +484,6 @@ private fun createDynamicLightColorScheme(context: Context): ColorScheme {
) )
} }
/**
* 创建深色颜色方案
*/
@Composable @Composable
private fun createDarkColorScheme() = darkColorScheme( private fun createDarkColorScheme() = darkColorScheme(
primary = ThemeConfig.currentTheme.primaryDark, primary = ThemeConfig.currentTheme.primaryDark,
@@ -347,9 +523,6 @@ private fun createDarkColorScheme() = darkColorScheme(
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark, surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark,
) )
/**
* 创建浅色颜色方案
*/
@Composable @Composable
private fun createLightColorScheme() = lightColorScheme( private fun createLightColorScheme() = lightColorScheme(
primary = ThemeConfig.currentTheme.primaryLight, primary = ThemeConfig.currentTheme.primaryLight,
@@ -389,218 +562,32 @@ private fun createLightColorScheme() = lightColorScheme(
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight, surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight,
) )
// 向后兼容
/** @OptIn(DelicateCoroutinesApi::class)
* 复制图片到应用内部存储并提升持久性
*/
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
return try {
val contentResolver: ContentResolver = contentResolver
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
val fileName = "custom_background.jpg"
val file = File(filesDir, fileName)
val backupFile = File(filesDir, "${fileName}.backup")
val outputStream = FileOutputStream(backupFile)
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()
if (file.exists()) {
file.delete()
}
backupFile.renameTo(file)
Uri.fromFile(file)
} catch (e: Exception) {
Log.e("ImageCopy", "复制图片失败: ${e.message}")
null
}
}
/**
* 保存并应用自定义背景
*/
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) { fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
val finalUri = if (transformation != null) { kotlinx.coroutines.GlobalScope.launch {
saveTransformedBackground(uri, transformation) BackgroundManager.saveAndApplyCustomBackground(this@saveAndApplyCustomBackground, uri, transformation)
} else {
copyImageToInternalStorage(uri)
} }
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("custom_background", finalUri?.toString())
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
}
ThemeConfig.customBackgroundUri = finalUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
CardConfig.cardElevation = 0.dp
CardConfig.isCustomBackgroundEnabled = true
} }
/**
* 保存自定义背景
*/
fun Context.saveCustomBackground(uri: Uri?) { fun Context.saveCustomBackground(uri: Uri?) {
val newUri = uri?.let { copyImageToInternalStorage(it) }
// 保存到配置文件
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.edit {
putString("custom_background", newUri?.toString())
if (uri == null) {
// 如果清除背景,也重置阻止刷新标志
putBoolean("prevent_background_refresh", false)
} else {
// 设置阻止刷新标志为false允许新设置的背景加载一次
putBoolean("prevent_background_refresh", false)
}
}
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
ThemeConfig.preventBackgroundRefresh = false
if (uri != null) { if (uri != null) {
CardConfig.cardElevation = 0.dp saveAndApplyCustomBackground(uri)
CardConfig.isCustomBackgroundEnabled = true } else {
BackgroundManager.clearCustomBackground(this)
} }
} }
/**
* 加载自定义背景
*/
fun Context.loadCustomBackground() {
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("custom_background", null)
val newUri = uriString?.toUri()
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("prevent_background_refresh", false)
ThemeConfig.preventBackgroundRefresh = preventRefresh
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
ThemeConfig.customBackgroundUri = newUri
ThemeConfig.backgroundImageLoaded = false
}
}
/**
* 保存主题模式
*/
fun Context.saveThemeMode(forceDark: Boolean?) { fun Context.saveThemeMode(forceDark: Boolean?) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) ThemeManager.saveThemeMode(this, forceDark)
.edit {
putString(
"theme_mode", when (forceDark) {
true -> "dark"
false -> "light"
null -> "system"
}
)
}
ThemeConfig.forceDarkMode = forceDark
ThemeConfig.needsResetOnThemeChange = forceDark == null
} }
/**
* 加载主题模式
*/
fun Context.loadThemeMode() {
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_mode", "system")
ThemeConfig.forceDarkMode = when(mode) {
"dark" -> true
"light" -> false
else -> null
}
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
}
/**
* 保存主题颜色
*/
fun Context.saveThemeColors(themeName: String) { fun Context.saveThemeColors(themeName: String) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) ThemeManager.saveThemeColors(this, themeName)
.edit {
putString("theme_colors", themeName)
}
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
} }
/**
* 加载主题颜色
*/
fun Context.loadThemeColors() {
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getString("theme_colors", "default")
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
}
/**
* 保存动态颜色状态
*/
fun Context.saveDynamicColorState(enabled: Boolean) { fun Context.saveDynamicColorState(enabled: Boolean) {
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) ThemeManager.saveDynamicColorState(this, enabled)
.edit {
putBoolean("use_dynamic_color", enabled)
}
ThemeConfig.useDynamicColor = enabled
}
/**
* 加载动态颜色状态
*/
fun Context.loadDynamicColorState() {
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
.getBoolean("use_dynamic_color", true)
ThemeConfig.useDynamicColor = enabled
}
@Composable
private fun SystemBarStyle(
darkMode: Boolean,
statusBarScrim: Color = Color.Transparent,
navigationBarScrim: Color = Color.Transparent,
) {
val context = LocalContext.current
val activity = context as ComponentActivity
SideEffect {
activity.enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
statusBarScrim.toArgb(),
statusBarScrim.toArgb(),
) { darkMode },
navigationBarStyle = when {
darkMode -> SystemBarStyle.dark(
navigationBarScrim.toArgb()
)
else -> SystemBarStyle.light(
navigationBarScrim.toArgb(),
navigationBarScrim.toArgb(),
)
}
)
}
} }

View File

@@ -1,24 +1,23 @@
package com.sukisu.ultra.ui.theme.component package com.sukisu.ultra.ui.theme.component
import android.net.Uri import android.net.Uri
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.*
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.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.material.icons.filled.Fullscreen
import androidx.compose.material3.Icon import androidx.compose.material3.*
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@@ -44,48 +43,62 @@ fun ImageEditorDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onConfirm: (Uri) -> Unit onConfirm: (Uri) -> Unit
) { ) {
var scale by remember { mutableFloatStateOf(1f) } // 图像变换状态
var offsetX by remember { mutableFloatStateOf(0f) } val transformState = remember { ImageTransformState() }
var offsetY by remember { mutableFloatStateOf(0f) }
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
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 imageSize by remember { mutableStateOf(Size.Zero) }
var screenSize by remember { mutableStateOf(Size.Zero) } var screenSize by remember { mutableStateOf(Size.Zero) }
// 动画状态
val animationSpec = spring<Float>(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
val animatedScale by animateFloatAsState( val animatedScale by animateFloatAsState(
targetValue = scale, targetValue = transformState.scale,
animationSpec = animationSpec,
label = "ScaleAnimation" label = "ScaleAnimation"
) )
val animatedOffsetX by animateFloatAsState( val animatedOffsetX by animateFloatAsState(
targetValue = offsetX, targetValue = transformState.offsetX,
animationSpec = animationSpec,
label = "OffsetXAnimation" label = "OffsetXAnimation"
) )
val animatedOffsetY by animateFloatAsState( val animatedOffsetY by animateFloatAsState(
targetValue = offsetY, targetValue = transformState.offsetY,
animationSpec = animationSpec,
label = "OffsetYAnimation" label = "OffsetYAnimation"
) )
val updateTransformation = remember {
{ newScale: Float, newOffsetX: Float, newOffsetY: Float -> // 工具函数
val scaleDiff = abs(newScale - lastScale)
val offsetXDiff = abs(newOffsetX - lastOffsetX)
val offsetYDiff = 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 { val scaleToFullScreen = remember {
{ {
if (imageSize.height > 0 && screenSize.height > 0) { if (imageSize.height > 0 && screenSize.height > 0) {
val newScale = screenSize.height / imageSize.height val newScale = screenSize.height / imageSize.height
updateTransformation(newScale, 0f, 0f) transformState.updateTransform(newScale, 0f, 0f)
}
}
}
val saveImage: () -> Unit = remember {
{
scope.launch {
try {
val transformation = BackgroundTransformation(
transformState.scale,
transformState.offsetX,
transformState.offsetY
)
val savedUri = context.saveTransformedBackground(imageUri, transformation)
savedUri?.let { onConfirm(it) }
} catch (_: Exception) {
}
} }
} }
} }
@@ -101,124 +114,298 @@ fun ImageEditorDialog(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.Black.copy(alpha = 0.9f)) .background(
Brush.radialGradient(
colors = listOf(
Color.Black.copy(alpha = 0.9f),
Color.Black.copy(alpha = 0.95f)
),
radius = 800f
)
)
.onSizeChanged { size -> .onSizeChanged { size ->
screenSize = Size(size.width.toFloat(), size.height.toFloat()) screenSize = Size(size.width.toFloat(), size.height.toFloat())
} }
) { ) {
AsyncImage( // 图像显示区域
model = ImageRequest.Builder(LocalContext.current) ImageDisplayArea(
.data(imageUri) imageUri = imageUri,
.crossfade(true) animatedScale = animatedScale,
.build(), animatedOffsetX = animatedOffsetX,
contentDescription = stringResource(R.string.settings_custom_background), animatedOffsetY = animatedOffsetY,
contentScale = ContentScale.Fit, transformState = transformState,
modifier = Modifier onImageSizeChanged = { imageSize = it },
.fillMaxSize() modifier = Modifier.fillMaxSize()
.graphicsLayer(
scaleX = animatedScale,
scaleY = animatedScale,
translationX = animatedOffsetX,
translationY = animatedOffsetY
)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
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 (_: Exception) {
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
}
}
}
}
.onSizeChanged { size ->
imageSize = Size(size.width.toFloat(), size.height.toFloat())
}
) )
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 = { 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 = {
scope.launch {
try {
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
val savedUri = context.saveTransformedBackground(imageUri, transformation)
savedUri?.let { onConfirm(it) }
} catch (_: Exception) {
""
}
}
},
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 TopToolbar(
.fillMaxWidth() onDismiss = onDismiss,
.padding(16.dp) onFullscreen = scaleToFullScreen,
.clip(RoundedCornerShape(8.dp)) onConfirm = saveImage,
.background(Color.Black.copy(alpha = 0.6f)) modifier = Modifier.align(Alignment.TopCenter)
.padding(16.dp) )
.align(Alignment.BottomCenter)
) { // 底部提示信息
Text( BottomHintCard(
text = stringResource(id = R.string.image_editor_hint), modifier = Modifier.align(Alignment.BottomCenter)
color = Color.White, )
style = MaterialTheme.typography.bodyMedium
)
}
} }
} }
} }
/**
* 图像变换状态管理类
*/
private class ImageTransformState {
var scale by mutableFloatStateOf(1f)
var offsetX by mutableFloatStateOf(0f)
var offsetY by mutableFloatStateOf(0f)
private var lastScale = 1f
private var lastOffsetX = 0f
private var lastOffsetY = 0f
fun updateTransform(newScale: Float, newOffsetX: Float, newOffsetY: Float) {
val scaleDiff = abs(newScale - lastScale)
val offsetXDiff = abs(newOffsetX - lastOffsetX)
val offsetYDiff = abs(newOffsetY - lastOffsetY)
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
scale = newScale
offsetX = newOffsetX
offsetY = newOffsetY
lastScale = newScale
lastOffsetX = newOffsetX
lastOffsetY = newOffsetY
}
}
fun resetToLast() {
scale = lastScale
offsetX = lastOffsetX
offsetY = lastOffsetY
}
}
/**
* 图像显示区域组件
*/
@Composable
private fun ImageDisplayArea(
imageUri: Uri,
animatedScale: Float,
animatedOffsetX: Float,
animatedOffsetY: Float,
transformState: ImageTransformState,
onImageSizeChanged: (Size) -> Unit,
modifier: Modifier = Modifier
) {
val scope = rememberCoroutineScope()
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUri)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.settings_custom_background),
contentScale = ContentScale.Fit,
modifier = modifier
.graphicsLayer(
scaleX = animatedScale,
scaleY = animatedScale,
translationX = animatedOffsetX,
translationY = animatedOffsetY
)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
scope.launch {
try {
val newScale = (transformState.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) {
(transformState.offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
} else 0f
val newOffsetY = if (maxOffsetY > 0) {
(transformState.offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
} else 0f
transformState.updateTransform(newScale, newOffsetX, newOffsetY)
} catch (_: Exception) {
transformState.resetToLast()
}
}
}
}
.onSizeChanged { size ->
onImageSizeChanged(Size(size.width.toFloat(), size.height.toFloat()))
}
)
}
/**
* 顶部工具栏组件
*/
@Composable
private fun TopToolbar(
onDismiss: () -> Unit,
onFullscreen: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(24.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
// 关闭按钮
ActionButton(
onClick = onDismiss,
icon = Icons.Default.Close,
contentDescription = stringResource(R.string.cancel),
backgroundColor = MaterialTheme.colorScheme.error.copy(alpha = 0.9f)
)
// 全屏按钮
ActionButton(
onClick = onFullscreen,
icon = Icons.Default.Fullscreen,
contentDescription = stringResource(R.string.reprovision),
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
)
// 确认按钮
ActionButton(
onClick = onConfirm,
icon = Icons.Default.Check,
contentDescription = stringResource(R.string.confirm),
backgroundColor = Color(0xFF4CAF50).copy(alpha = 0.9f)
)
}
}
/**
* 操作按钮组件
*/
@Composable
private fun ActionButton(
onClick: () -> Unit,
icon: androidx.compose.ui.graphics.vector.ImageVector,
contentDescription: String,
backgroundColor: Color,
modifier: Modifier = Modifier
) {
var isPressed by remember { mutableStateOf(false) }
val buttonScale by animateFloatAsState(
targetValue = if (isPressed) 0.85f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "ButtonScale"
)
val buttonAlpha by animateFloatAsState(
targetValue = if (isPressed) 0.8f else 1f,
animationSpec = tween(100),
label = "ButtonAlpha"
)
Surface(
onClick = {
isPressed = true
onClick()
},
modifier = modifier
.size(64.dp)
.graphicsLayer(
scaleX = buttonScale,
scaleY = buttonScale,
alpha = buttonAlpha
),
shape = CircleShape,
color = backgroundColor,
shadowElevation = 8.dp
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
}
LaunchedEffect(isPressed) {
if (isPressed) {
kotlinx.coroutines.delay(150)
isPressed = false
}
}
}
/**
* 底部提示卡片组件
*/
@Composable
private fun BottomHintCard(
modifier: Modifier = Modifier
) {
var isVisible by remember { mutableStateOf(true) }
val cardAlpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(
durationMillis = 500,
easing = EaseInOutCubic
),
label = "HintAlpha"
)
val cardTranslationY by animateFloatAsState(
targetValue = if (isVisible) 0f else 100f,
animationSpec = tween(
durationMillis = 500,
easing = EaseInOutCubic
),
label = "HintTranslation"
)
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(4000)
isVisible = false
}
Card(
modifier = modifier
.fillMaxWidth()
.padding(24.dp)
.alpha(cardAlpha)
.graphicsLayer {
translationY = cardTranslationY
},
colors = CardDefaults.cardColors(
containerColor = Color.Black.copy(alpha = 0.85f)
),
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
Text(
text = stringResource(id = R.string.image_editor_hint),
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.padding(20.dp)
.fillMaxWidth()
)
}
}

View File

@@ -105,26 +105,24 @@ class MoreSettingsHandlers(
else -> null else -> null
} }
context.saveThemeMode(newThemeMode) context.saveThemeMode(newThemeMode)
ThemeConfig.updateTheme(darkMode = newThemeMode)
when (index) { when (index) {
2 -> { // 深色 2 -> { // 深色
ThemeConfig.forceDarkMode = true ThemeConfig.updateTheme(darkMode = true)
CardConfig.isUserDarkModeEnabled = true CardConfig.updateThemePreference(darkMode = true, lightMode = false)
CardConfig.isUserLightModeEnabled = false
CardConfig.setThemeDefaults(true) CardConfig.setThemeDefaults(true)
CardConfig.save(context) CardConfig.save(context)
} }
1 -> { // 浅色 1 -> { // 浅色
ThemeConfig.forceDarkMode = false ThemeConfig.updateTheme(darkMode = false)
CardConfig.isUserLightModeEnabled = true CardConfig.updateThemePreference(darkMode = false, lightMode = true)
CardConfig.isUserDarkModeEnabled = false
CardConfig.setThemeDefaults(false) CardConfig.setThemeDefaults(false)
CardConfig.save(context) CardConfig.save(context)
} }
0 -> { // 跟随系统 0 -> { // 跟随系统
ThemeConfig.forceDarkMode = null ThemeConfig.updateTheme(darkMode = null)
CardConfig.isUserLightModeEnabled = false CardConfig.updateThemePreference(darkMode = null, lightMode = null)
CardConfig.isUserDarkModeEnabled = false
val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
CardConfig.setThemeDefaults(isNightModeActive) CardConfig.setThemeDefaults(isNightModeActive)
CardConfig.save(context) CardConfig.save(context)
@@ -145,6 +143,7 @@ class MoreSettingsHandlers(
ThemeColors.Yellow -> "yellow" ThemeColors.Yellow -> "yellow"
else -> "default" else -> "default"
}) })
ThemeConfig.updateTheme(theme = theme)
} }
/** /**
@@ -153,6 +152,7 @@ class MoreSettingsHandlers(
fun handleDynamicColorChange(enabled: Boolean) { fun handleDynamicColorChange(enabled: Boolean) {
state.useDynamicColor = enabled state.useDynamicColor = enabled
context.saveDynamicColorState(enabled) context.saveDynamicColorState(enabled)
ThemeConfig.updateTheme(dynamicColor = enabled)
} }
/** /**
@@ -222,8 +222,6 @@ class MoreSettingsHandlers(
CardConfig.isCustomDimSet = false CardConfig.isCustomDimSet = false
CardConfig.isCustomBackgroundEnabled = false CardConfig.isCustomBackgroundEnabled = false
saveCardConfig(context) saveCardConfig(context)
ThemeConfig.needsResetOnThemeChange = true
ThemeConfig.preventBackgroundRefresh = false ThemeConfig.preventBackgroundRefresh = false
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {