feat: 7种牌桌背景+7种卡面风格下拉选择,页面切换动画丝滑过渡

This commit is contained in:
flykhan 2026-04-26 19:00:30 +08:00
parent 167ebdff27
commit 8f843a4e6f
5 changed files with 314 additions and 57 deletions

View File

@ -4,12 +4,15 @@ import android.os.Bundle
import android.content.pm.ActivityInfo
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavType
import androidx.navigation.compose.*
import androidx.navigation.navArgument
@ -191,10 +194,18 @@ fun UnoApp() {
LocalCardTheme provides cardTheme,
LocalTableBg provides tableBg
) {
val enterAnim: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition) = {
fadeIn(animationSpec = tween(300)) + slideInHorizontally(animationSpec = tween(350)) { it / 4 }
}
val exitAnim: (AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition) = {
fadeOut(animationSpec = tween(300)) + slideOutHorizontally(animationSpec = tween(350)) { -it / 4 }
}
NavHost(
navController = navController,
startDestination = Screen.MainMenu.route,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
enterTransition = enterAnim,
exitTransition = exitAnim
) {
composable(Screen.MainMenu.route) {
MainMenuScreen(
@ -416,15 +427,13 @@ fun UnoApp() {
savedName = name
prefs.edit().putString("player_name", name).apply()
},
onToggleTheme = {
val next = CardTheme.values()[(cardTheme.ordinal + 1) % CardTheme.values().size]
cardTheme = next
CardTheme.save(context, next)
onSetTheme = { theme ->
cardTheme = theme
CardTheme.save(context, theme)
},
onToggleBg = {
val next = TableBg.values()[(tableBg.ordinal + 1) % TableBg.values().size]
tableBg = next
TableBg.save(context, next)
onSetBg = { bg ->
tableBg = bg
TableBg.save(context, bg)
},
onToggleOrientation = {
isLandscape = !isLandscape

View File

@ -16,7 +16,6 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
@ -44,6 +43,10 @@ fun CardView(
CardTheme.CLASSIC -> ClassicCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.ELEGANT -> ElegantCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.MIDNIGHT -> MidnightCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.NEON -> NeonCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.PASTEL -> PastelCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.FOREST -> ForestCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
CardTheme.OCEAN -> OceanCard(card, cardBg, isWild, modifier, selected, playable, onClick, onLongPress = { showInfo = true })
}
if (showInfo) {
@ -94,7 +97,6 @@ private fun ElegantCard(
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
// Inner colored oval
Box(
modifier = Modifier
.fillMaxWidth(0.82f).fillMaxHeight(0.78f)
@ -105,14 +107,10 @@ private fun ElegantCard(
),
contentAlignment = Alignment.Center
) {
Text(
text = card.displayText,
color = Color.White,
Text(card.displayText, color = Color.White,
fontSize = if (card.type == CardType.NUMBER && card.number >= 10) 16.sp else 22.sp,
fontWeight = FontWeight.Black
)
fontWeight = FontWeight.Black)
}
// Corner numbers
Text(card.displayText, modifier = Modifier.align(Alignment.TopStart).padding(6.dp, 4.dp),
color = if (isWild) Color.White else cardBg, fontSize = 10.sp, fontWeight = FontWeight.Bold)
Text(card.displayText, modifier = Modifier.align(Alignment.BottomEnd).padding(6.dp, 4.dp),
@ -140,17 +138,75 @@ private fun MidnightCard(
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(
text = card.displayText,
color = if (isWild) Color.Magenta else getCardColor(card.color.name),
fontSize = 24.sp,
fontWeight = FontWeight.Black
)
Text(card.displayText, color = if (isWild) Color.Magenta else getCardColor(card.color.name),
fontSize = 24.sp, fontWeight = FontWeight.Black)
if (card.color != CardColor.WILD) {
// mini color dots
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
repeat(3) {
Box(Modifier.size(3.dp).clip(CircleShape).background(getCardColor(card.color.name)))
repeat(3) { Box(Modifier.size(3.dp).clip(CircleShape).background(getCardColor(card.color.name))) }
}
}
}
}
}
// ── Neon ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun NeonCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
val neonColor = if (isWild) Color.Magenta else cardBg
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-10).dp) else Modifier)
.shadow(if (selected) 10.dp else 4.dp, RoundedCornerShape(8.dp), ambientColor = neonColor, spotColor = neonColor)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF0A0A0A))
.border(2.dp, neonColor, RoundedCornerShape(8.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Text(card.displayText, color = neonColor, fontSize = 26.sp, fontWeight = FontWeight.Black)
if (!isWild) {
Text(card.displayText, modifier = Modifier.align(Alignment.TopStart).padding(6.dp, 4.dp),
color = neonColor.copy(alpha = 0.6f), fontSize = 9.sp)
Text(card.displayText, modifier = Modifier.align(Alignment.BottomEnd).padding(6.dp, 4.dp),
color = neonColor.copy(alpha = 0.6f), fontSize = 9.sp)
}
}
}
// ── Pastel ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PastelCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-10).dp) else Modifier)
.shadow(if (selected) 6.dp else 2.dp, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.background(
if (isWild) Brush.horizontalGradient(listOf(UnoRed.copy(alpha = 0.15f), UnoBlue.copy(alpha = 0.15f),
UnoGreen.copy(alpha = 0.15f), UnoYellow.copy(alpha = 0.15f)))
else Brush.verticalGradient(listOf(Color.White, cardBg.copy(alpha = 0.08f)))
)
.border(1.5.dp, cardBg.copy(alpha = 0.3f), RoundedCornerShape(16.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(card.displayText, color = cardBg, fontSize = 26.sp, fontWeight = FontWeight.Bold)
if (isWild) {
Spacer(modifier = Modifier.height(2.dp))
Row(Modifier.fillMaxWidth(0.6f), horizontalArrangement = Arrangement.SpaceEvenly) {
listOf(UnoRed, UnoBlue, UnoGreen, UnoYellow).forEach { c ->
Box(Modifier.size(5.dp).clip(CircleShape).background(c))
}
}
}
@ -158,6 +214,64 @@ private fun MidnightCard(
}
}
// ── Forest ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ForestCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-10).dp) else Modifier)
.shadow(if (selected) 6.dp else 2.dp, RoundedCornerShape(6.dp))
.clip(RoundedCornerShape(6.dp))
.background(
if (isWild) Brush.verticalGradient(listOf(Color(0xFF2E7D32), Color(0xFF1B5E20)))
else Brush.verticalGradient(listOf(Color(0xFFE8F5E9), Color(0xFFC8E6C9)))
)
.border(1.5.dp, if (isWild) Color(0xFF81C784) else Color(0xFF388E3C).copy(alpha = 0.3f), RoundedCornerShape(6.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Text(card.displayText, color = if (isWild) Color.White else cardBg, fontSize = 24.sp, fontWeight = FontWeight.Black)
}
}
// ── Ocean ──
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun OceanCard(
card: Card, cardBg: Color, isWild: Boolean,
modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit
) {
Box(
modifier = modifier
.width(60.dp).height(90.dp)
.then(if (selected) Modifier.offset(y = (-10).dp) else Modifier)
.shadow(if (selected) 6.dp else 3.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(
if (isWild) Brush.verticalGradient(listOf(Color(0xFF0277BD), Color(0xFF00BCD4), Color(0xFF0288D1)))
else Brush.verticalGradient(listOf(cardBg.copy(alpha = 0.7f), cardBg))
)
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(10.dp))
.then(Modifier.combinedClickable(onClick = onClick, onLongClick = onLongPress)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(card.displayText, color = Color.White, fontSize = 26.sp, fontWeight = FontWeight.Black)
if (!isWild) {
Spacer(modifier = Modifier.height(2.dp))
Row(horizontalArrangement = Arrangement.spacedBy(3.dp)) {
repeat(3) { Box(Modifier.size(4.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.5f))) }
}
}
}
}
}
@Composable
fun CardInfoPopup(card: Card, onDismiss: () -> Unit) {
val effectText = when (card.type) {
@ -209,15 +323,12 @@ fun CardInfoPopup(card: Card, onDismiss: () -> Unit) {
}
}
// ── Shared inner content for classic ──
@Composable
private fun CardContent(card: Card, isWild: Boolean) {
if (isWild) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(
text = if (card.type == CardType.WILD_DRAW_FOUR) "+4" else "W",
color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Black
)
Text(if (card.type == CardType.WILD_DRAW_FOUR) "+4" else "W", color = Color.White,
fontSize = 18.sp, fontWeight = FontWeight.Black)
Spacer(modifier = Modifier.height(4.dp))
Row(Modifier.fillMaxWidth(0.7f), horizontalArrangement = Arrangement.SpaceEvenly) {
listOf(UnoRed, UnoBlue, UnoGreen, UnoYellow).forEach { c ->
@ -267,12 +378,8 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu
.border(2.dp, GoldAccent, RoundedCornerShape(14.dp)),
contentAlignment = Alignment.Center
) {
Box(
Modifier.fillMaxWidth(0.75f).fillMaxHeight(0.7f)
.clip(RoundedCornerShape(40))
.background(DarkCard),
contentAlignment = Alignment.Center
) {
Box(Modifier.fillMaxWidth(0.75f).fillMaxHeight(0.7f).clip(RoundedCornerShape(40)).background(DarkCard),
contentAlignment = Alignment.Center) {
Text("UNO", color = GoldAccent, fontSize = 16.sp, fontWeight = FontWeight.Black)
}
}
@ -285,9 +392,47 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu
.background(DarkCard)
.border(1.5.dp, Color.Magenta.copy(alpha = 0.5f), RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) {
Text("U", color = Color.Magenta, fontSize = 32.sp, fontWeight = FontWeight.Black)
}
) { Text("U", color = Color.Magenta, fontSize = 32.sp, fontWeight = FontWeight.Black) }
}
CardTheme.NEON -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(4.dp, RoundedCornerShape(8.dp), ambientColor = Color.Cyan)
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFF0A0A0A))
.border(2.dp, Color.Cyan, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color.Cyan, fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
CardTheme.PASTEL -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(2.dp, RoundedCornerShape(16.dp))
.clip(RoundedCornerShape(16.dp))
.background(Brush.verticalGradient(listOf(Color(0xFFFFF3E0), Color(0xFFFCE4EC))))
.border(1.5.dp, Color(0xFFE0C0C0), RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color(0xFFCC9999), fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
CardTheme.FOREST -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(2.dp, RoundedCornerShape(6.dp))
.clip(RoundedCornerShape(6.dp))
.background(Brush.verticalGradient(listOf(Color(0xFF2E7D32), Color(0xFF1B5E20))))
.border(1.5.dp, Color(0xFF81C784), RoundedCornerShape(6.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
CardTheme.OCEAN -> {
Box(
modifier = modifier.width(60.dp).height(90.dp)
.shadow(3.dp, RoundedCornerShape(10.dp))
.clip(RoundedCornerShape(10.dp))
.background(Brush.verticalGradient(listOf(Color(0xFF0277BD), Color(0xFF00BCD4))))
.border(1.dp, Color.White.copy(alpha = 0.3f), RoundedCornerShape(10.dp)),
contentAlignment = Alignment.Center
) { Text("UNO", color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.Black) }
}
}
}

View File

@ -1,6 +1,7 @@
package com.unogame.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -12,10 +13,15 @@ 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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.unogame.model.CardColor
import com.unogame.model.CardType
import com.unogame.model.Card
import com.unogame.ui.components.CardView
import com.unogame.ui.theme.*
val HUMAN_NAME_PRESETS = listOf(
@ -26,6 +32,8 @@ val HUMAN_NAME_PRESETS = listOf(
"低带宽生物", "情感过载体"
)
val SAMPLE_CARD = Card(CardColor.RED, CardType.NUMBER, 7)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
@ -34,13 +42,15 @@ fun SettingsScreen(
currentBg: TableBg,
isLandscape: Boolean,
onNameChanged: (String) -> Unit,
onToggleTheme: () -> Unit,
onToggleBg: () -> Unit,
onSetTheme: (CardTheme) -> Unit,
onSetBg: (TableBg) -> Unit,
onToggleOrientation: () -> Unit,
onBack: () -> Unit
) {
var playerName by remember { mutableStateOf(initialName) }
var showAbout by remember { mutableStateOf(false) }
var themeExpanded by remember { mutableStateOf(false) }
var bgExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxSize().background(LocalTableBg.current.color)) {
Column(
@ -120,21 +130,106 @@ fun SettingsScreen(
Spacer(modifier = Modifier.height(28.dp))
// Card theme
// Appearance
Text("外观设置", color = Color.White.copy(alpha = 0.6f), fontSize = 14.sp)
Spacer(modifier = Modifier.height(12.dp))
SettingsRow(
icon = Icons.Default.Palette,
label = "卡面风格",
value = currentTheme.displayName,
onClick = onToggleTheme
)
SettingsRow(
icon = Icons.Default.Wallpaper,
label = "牌桌背景",
value = currentBg.displayName,
onClick = onToggleBg
)
// Card theme dropdown
ExposedDropdownMenuBox(
expanded = themeExpanded,
onExpandedChange = { themeExpanded = it }
) {
OutlinedTextField(
value = currentTheme.displayName,
onValueChange = {},
readOnly = true,
label = { Text("卡面风格") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = themeExpanded) },
modifier = Modifier.fillMaxWidth().menuAnchor(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedLabelColor = GoldAccent,
unfocusedLabelColor = Color.White.copy(alpha = 0.6f),
focusedBorderColor = GoldAccent,
unfocusedBorderColor = Color.Gray,
cursorColor = GoldAccent
)
)
ExposedDropdownMenu(
expanded = themeExpanded,
onDismissRequest = { themeExpanded = false }
) {
CardTheme.values().forEach { theme ->
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
CompositionLocalProvider(LocalCardTheme provides theme) {
CardView(card = SAMPLE_CARD, selected = false, playable = false, onClick = {},
modifier = Modifier.size(30.dp, 45.dp))
}
Spacer(modifier = Modifier.width(12.dp))
Text(theme.displayName, fontSize = 14.sp)
}
},
onClick = { onSetTheme(theme); themeExpanded = false }
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// Table bg dropdown
ExposedDropdownMenuBox(
expanded = bgExpanded,
onExpandedChange = { bgExpanded = it }
) {
OutlinedTextField(
value = currentBg.displayName,
onValueChange = {},
readOnly = true,
label = { Text("牌桌背景") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = bgExpanded) },
modifier = Modifier.fillMaxWidth().menuAnchor(),
colors = OutlinedTextFieldDefaults.colors(
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedLabelColor = GoldAccent,
unfocusedLabelColor = Color.White.copy(alpha = 0.6f),
focusedBorderColor = GoldAccent,
unfocusedBorderColor = Color.Gray,
cursorColor = GoldAccent
)
)
ExposedDropdownMenu(
expanded = bgExpanded,
onDismissRequest = { bgExpanded = false }
) {
TableBg.values().forEach { bg ->
DropdownMenuItem(
text = {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(30.dp)
.clip(RoundedCornerShape(6.dp))
.border(1.dp, Color.White.copy(alpha = 0.2f), RoundedCornerShape(6.dp))
.background(bg.color)
)
Spacer(modifier = Modifier.width(12.dp))
Text(bg.displayName, fontSize = 14.sp)
}
},
onClick = { onSetBg(bg); bgExpanded = false }
)
}
}
}
Spacer(modifier = Modifier.height(12.dp))
// Orientation
SettingsRow(
icon = if (isLandscape) Icons.Default.StayCurrentLandscape else Icons.Default.StayCurrentPortrait,
label = "屏幕方向",

View File

@ -5,7 +5,11 @@ import android.content.Context
enum class CardTheme(val displayName: String) {
CLASSIC("经典"),
ELEGANT("优雅"),
MIDNIGHT("暗夜");
MIDNIGHT("暗夜"),
NEON("霓虹"),
PASTEL("马卡龙"),
FOREST("森林"),
OCEAN("海洋");
companion object {
private const val KEY = "card_theme"

View File

@ -6,7 +6,11 @@ import androidx.compose.ui.graphics.Color
enum class TableBg(val displayName: String, val color: Color) {
DARK("暗黑", Color(0xFF121212)),
GREEN("墨绿", Color(0xFF1A3C2A)),
BLUE("深蓝", Color(0xFF0D1B2A));
BLUE("深蓝", Color(0xFF0D1B2A)),
PURPLE("暗紫", Color(0xFF1A1035)),
RED("酒红", Color(0xFF2D1111)),
TEAL("深青", Color(0xFF0D2B2A)),
CHARCOAL("炭灰", Color(0xFF1E1E1E));
companion object {
private const val KEY = "table_bg"