fix(ui): 修复照片编辑页错位、亮色主题统一与添加切换过渡动画
- 切换动画:ToolPanel 引入 AnimatedContent,在 旋转 / 裁剪 / 涂鸦 模式切换时平滑淡入滑动过渡 - 修复错位:将“撤销”按钮从限制宽高的 OutlinedIconButton 改为 OutlinedButton(contentPadding),解决“撤销”汉字被压缩竖向换行错位的问题 - 亮色主题统一:将界面配色调整为与鸭鸭日历应用一致的亮色 Material 3 Theme (MaterialTheme.colorScheme.surface / surfaceContainer)
This commit is contained in:
parent
088860861e
commit
0a6b9449a3
@ -1,9 +1,15 @@
|
|||||||
package plus.rua.project.ui
|
package plus.rua.project.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
@ -14,6 +20,7 @@ import androidx.compose.foundation.gestures.detectDragGestures
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
@ -34,7 +41,6 @@ import androidx.compose.material.icons.outlined.RotateLeft
|
|||||||
import androidx.compose.material.icons.outlined.RotateRight
|
import androidx.compose.material.icons.outlined.RotateRight
|
||||||
import androidx.compose.material.icons.outlined.Undo
|
import androidx.compose.material.icons.outlined.Undo
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@ -43,7 +49,7 @@ import androidx.compose.material3.FilledTonalButton
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedIconButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SegmentedButton
|
import androidx.compose.material3.SegmentedButton
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
@ -119,9 +125,6 @@ fun PhotoEditorScreen(
|
|||||||
LaunchedEffect(savedPath) { onSaved(savedPath) }
|
LaunchedEffect(savedPath) { onSaved(savedPath) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val darkBackground = Color(0xFF121316)
|
|
||||||
val topBarContainer = Color(0xFF1A1B1F)
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.semantics { testTagsAsResourceId = true },
|
modifier = modifier.semantics { testTagsAsResourceId = true },
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -129,16 +132,14 @@ fun PhotoEditorScreen(
|
|||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = "照片图层编辑",
|
text = "照片图层编辑",
|
||||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.SemiBold)
|
||||||
color = Color.White
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "返回",
|
contentDescription = "返回"
|
||||||
tint = Color.White
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -158,10 +159,10 @@ fun PhotoEditorScreen(
|
|||||||
Text("完成", fontWeight = FontWeight.Bold)
|
Text("完成", fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = topBarContainer)
|
colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = darkBackground
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -172,7 +173,7 @@ fun PhotoEditorScreen(
|
|||||||
state.loading -> Box(
|
state.loading -> Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) { CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) }
|
) { CircularProgressIndicator() }
|
||||||
|
|
||||||
state.error != null -> Box(
|
state.error != null -> Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -231,7 +232,6 @@ private fun EditorBody(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(Color(0xFF1A1B1F))
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||||
@ -240,12 +240,6 @@ private fun EditorBody(
|
|||||||
selected = activeTab == tab,
|
selected = activeTab == tab,
|
||||||
onClick = { onTabChange(tab) },
|
onClick = { onTabChange(tab) },
|
||||||
shape = SegmentedButtonDefaults.itemShape(index, EditTab.entries.size),
|
shape = SegmentedButtonDefaults.itemShape(index, EditTab.entries.size),
|
||||||
colors = SegmentedButtonDefaults.colors(
|
|
||||||
activeContainerColor = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
activeContentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
inactiveContainerColor = Color(0xFF25262B),
|
|
||||||
inactiveContentColor = Color.LightGray
|
|
||||||
),
|
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
when (tab) {
|
when (tab) {
|
||||||
@ -283,8 +277,8 @@ private fun EditorBody(
|
|||||||
Card(
|
Card(
|
||||||
shape = RoundedCornerShape(20.dp),
|
shape = RoundedCornerShape(20.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.Black),
|
colors = CardDefaults.cardColors(containerColor = Color.Black),
|
||||||
border = BorderStroke(1.dp, Color(0xFF2C2D32)),
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@ -303,7 +297,7 @@ private fun EditorBody(
|
|||||||
|
|
||||||
if (saving) {
|
if (saving) {
|
||||||
Surface(
|
Surface(
|
||||||
color = Color.Black.copy(alpha = 0.6f),
|
color = Color.Black.copy(alpha = 0.7f),
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
@ -326,9 +320,18 @@ private fun EditorBody(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tool Control Panel at Bottom
|
// Tool Control Panel with Smooth Animated Transition
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = activeTab,
|
||||||
|
transitionSpec = {
|
||||||
|
(slideInHorizontally { width -> if (targetState > initialState) width else -width } + fadeIn(tween(220)))
|
||||||
|
.togetherWith(slideOutHorizontally { width -> if (targetState > initialState) -width else width } + fadeOut(tween(180)))
|
||||||
|
},
|
||||||
|
label = "tabTransition",
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) { targetMode ->
|
||||||
ToolPanel(
|
ToolPanel(
|
||||||
mode = activeTab,
|
mode = targetMode,
|
||||||
state = state,
|
state = state,
|
||||||
onRotate = onRotate,
|
onRotate = onRotate,
|
||||||
onCropToggle = onCropToggle,
|
onCropToggle = onCropToggle,
|
||||||
@ -338,6 +341,7 @@ private fun EditorBody(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -603,8 +607,9 @@ private fun ToolPanel(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
color = Color(0xFF1A1B1F),
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
shape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||||
|
tonalElevation = 3.dp,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@ -622,11 +627,7 @@ private fun ToolPanel(
|
|||||||
) {
|
) {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = { onRotate(-90) },
|
onClick = { onRotate(-90) },
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp)
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = Color(0xFF2B2C32),
|
|
||||||
contentColor = Color.White
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.RotateLeft,
|
imageVector = Icons.Outlined.RotateLeft,
|
||||||
@ -638,9 +639,9 @@ private fun ToolPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = Color(0xFF26272C),
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
border = BorderStroke(1.dp, Color(0xFF3A3B42))
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${state.rotationDegrees}°",
|
text = "${state.rotationDegrees}°",
|
||||||
@ -653,11 +654,7 @@ private fun ToolPanel(
|
|||||||
|
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = { onRotate(90) },
|
onClick = { onRotate(90) },
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp)
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = Color(0xFF2B2C32),
|
|
||||||
contentColor = Color.White
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.RotateRight,
|
imageVector = Icons.Outlined.RotateRight,
|
||||||
@ -679,7 +676,7 @@ private fun ToolPanel(
|
|||||||
Text(
|
Text(
|
||||||
text = if (state.cropEnabled) "拖动四角或框架调整裁剪区域" else "点击开启自由裁剪",
|
text = if (state.cropEnabled) "拖动四角或框架调整裁剪区域" else "点击开启自由裁剪",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color.LightGray
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@ -730,34 +727,32 @@ private fun ToolPanel(
|
|||||||
Text(
|
Text(
|
||||||
text = "涂鸦画笔颜色",
|
text = "涂鸦画笔颜色",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = Color.LightGray
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
OutlinedIconButton(
|
// 使用 OutlinedButton 避免数字/汉字在宽高度受限的 IconButtons 里错位换行
|
||||||
|
OutlinedButton(
|
||||||
onClick = onUndoStroke,
|
onClick = onUndoStroke,
|
||||||
enabled = state.strokes.isNotEmpty(),
|
enabled = state.strokes.isNotEmpty(),
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
border = BorderStroke(1.dp, if (state.strokes.isNotEmpty()) Color.White else Color.Gray)
|
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 6.dp),
|
||||||
) {
|
border = BorderStroke(
|
||||||
Row(
|
1.dp,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
if (state.strokes.isNotEmpty()) MaterialTheme.colorScheme.outline else MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Undo,
|
imageVector = Icons.Outlined.Undo,
|
||||||
contentDescription = "撤销笔触",
|
contentDescription = "撤销笔触",
|
||||||
tint = if (state.strokes.isNotEmpty()) Color.White else Color.Gray,
|
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "撤销",
|
text = "撤销",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelMedium
|
||||||
color = if (state.strokes.isNotEmpty()) Color.White else Color.Gray
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val palette = listOf(
|
val palette = listOf(
|
||||||
Color(0xFFFF453A), // Red
|
Color(0xFFFF453A), // Red
|
||||||
@ -765,8 +760,8 @@ private fun ToolPanel(
|
|||||||
Color(0xFFFFD60A), // Yellow
|
Color(0xFFFFD60A), // Yellow
|
||||||
Color(0xFF30D158), // Green
|
Color(0xFF30D158), // Green
|
||||||
Color(0xFF64D2FF), // Cyan
|
Color(0xFF64D2FF), // Cyan
|
||||||
Color(0xFFFFFFFF), // White
|
Color(0xFF007AFF), // Blue
|
||||||
Color(0xFF000000) // Black
|
Color(0xFF1D1B20) // Dark Gray / Black
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@ -782,7 +777,7 @@ private fun ToolPanel(
|
|||||||
.background(c, CircleShape)
|
.background(c, CircleShape)
|
||||||
.border(
|
.border(
|
||||||
width = if (selected) 2.5.dp else 1.dp,
|
width = if (selected) 2.5.dp else 1.dp,
|
||||||
color = if (selected) MaterialTheme.colorScheme.primary else Color.Gray,
|
color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant,
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
)
|
)
|
||||||
.clickable { onStrokeColorChange(c) },
|
.clickable { onStrokeColorChange(c) },
|
||||||
@ -792,7 +787,7 @@ private fun ToolPanel(
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Check,
|
imageVector = Icons.Filled.Check,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (c == Color.White || c == Color(0xFFFFD60A)) Color.Black else Color.White,
|
tint = if (c == Color(0xFFFFD60A) || c == Color(0xFF64D2FF)) Color.Black else Color.White,
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(14.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user