diff --git a/app/src/main/java/com/unogame/MainActivity.kt b/app/src/main/java/com/unogame/MainActivity.kt index 19bf9f7..aea39ae 100644 --- a/app/src/main/java/com/unogame/MainActivity.kt +++ b/app/src/main/java/com/unogame/MainActivity.kt @@ -70,6 +70,8 @@ fun UnoApp() { var cardTheme by remember { mutableStateOf(CardTheme.load(context)) } var tableBg by remember { mutableStateOf(TableBg.load(context)) } var isLandscape by remember { mutableStateOf(prefs.getBoolean("landscape", false)) } + var cardScale by remember { mutableStateOf(loadCardScale(context)) } + var handOffset by remember { mutableStateOf(loadHandOffset(context)) } // Apply orientation on start LaunchedEffect(Unit) { @@ -192,7 +194,8 @@ fun UnoApp() { CompositionLocalProvider( LocalCardTheme provides cardTheme, - LocalTableBg provides tableBg + LocalTableBg provides tableBg, + LocalCardScale provides cardScale ) { val enterAnim: (AnimatedContentTransitionScope.() -> EnterTransition) = { fadeIn(animationSpec = tween(300)) + slideInHorizontally(animationSpec = tween(350)) { it / 4 } @@ -401,6 +404,7 @@ fun UnoApp() { totalPlayers = totalPlayers, humanPlayerName = humanPlayerName, botNames = botNames, + handOffset = handOffset, onBackToMenu = { navController.navigate(Screen.MainMenu.route) { popUpTo(0) { inclusive = true } @@ -423,6 +427,7 @@ fun UnoApp() { currentTheme = cardTheme, currentBg = tableBg, isLandscape = isLandscape, + handOffset = handOffset, onNameChanged = { name -> savedName = name prefs.edit().putString("player_name", name).apply() @@ -435,6 +440,10 @@ fun UnoApp() { tableBg = bg TableBg.save(context, bg) }, + onSetHandOffset = { offset -> + handOffset = offset + saveHandOffset(context, offset) + }, onToggleOrientation = { isLandscape = !isLandscape prefs.edit().putBoolean("landscape", isLandscape).apply() diff --git a/app/src/main/java/com/unogame/ui/components/CardView.kt b/app/src/main/java/com/unogame/ui/components/CardView.kt index 765891d..a51cd09 100644 --- a/app/src/main/java/com/unogame/ui/components/CardView.kt +++ b/app/src/main/java/com/unogame/ui/components/CardView.kt @@ -16,6 +16,7 @@ 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.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup @@ -32,21 +33,25 @@ fun CardView( selected: Boolean = false, playable: Boolean = true, theme: CardTheme = LocalCardTheme.current, + scale: Float = LocalCardScale.current, onClick: () -> Unit = {} ) { val cardBg = getCardBgColor(card.color.name) val isWild = card.color == CardColor.WILD var showInfo by remember { mutableStateOf(false) } + val w = (60 * scale).dp + val h = (90 * scale).dp + val off = (-30 * scale).dp Box { when (theme) { - 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 }) + CardTheme.CLASSIC -> ClassicCard(card, cardBg, isWild, w, h, off, modifier, selected, playable, onClick, onLongPress = { showInfo = true }) + CardTheme.ELEGANT -> ElegantCard(card, cardBg, isWild, w, h, off, modifier, selected, playable, onClick, onLongPress = { showInfo = true }) + CardTheme.MIDNIGHT -> MidnightCard(card, cardBg, isWild, w, h, off, modifier, selected, playable, onClick, onLongPress = { showInfo = true }) + CardTheme.NEON -> NeonCard(card, cardBg, isWild, w, h, off, modifier, selected, playable, onClick, onLongPress = { showInfo = true }) + CardTheme.PASTEL -> PastelCard(card, cardBg, isWild, w, h, off, modifier, selected, playable, onClick, onLongPress = { showInfo = true }) + CardTheme.FOREST -> ForestCard(card, cardBg, isWild, w, h, off, modifier, selected, playable, onClick, onLongPress = { showInfo = true }) + CardTheme.OCEAN -> OceanCard(card, cardBg, isWild, w, h, off, modifier, selected, playable, onClick, onLongPress = { showInfo = true }) } if (showInfo) { @@ -59,13 +64,13 @@ fun CardView( @OptIn(ExperimentalFoundationApi::class) @Composable private fun ClassicCard( - card: Card, cardBg: Color, isWild: Boolean, + card: Card, cardBg: Color, isWild: Boolean, w: Dp, h: Dp, off: Dp, 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 = (-30).dp) else Modifier) + .width(w).height(h) + .then(if (selected) Modifier.offset(y = off) else Modifier) .shadow(if (selected) 8.dp else 2.dp, RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp)) .background( @@ -82,14 +87,14 @@ private fun ClassicCard( @OptIn(ExperimentalFoundationApi::class) @Composable private fun ElegantCard( - card: Card, cardBg: Color, isWild: Boolean, + card: Card, cardBg: Color, isWild: Boolean, w: Dp, h: Dp, off: Dp, modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit ) { val borderColor = if (isWild) GoldAccent else cardBg.copy(alpha = 0.5f) Box( modifier = modifier - .width(60.dp).height(90.dp) - .then(if (selected) Modifier.offset(y = (-30).dp) else Modifier) + .width(w).height(h) + .then(if (selected) Modifier.offset(y = off) else Modifier) .shadow(if (selected) 8.dp else 3.dp, RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp)) .background(Color.White) @@ -122,14 +127,14 @@ private fun ElegantCard( @OptIn(ExperimentalFoundationApi::class) @Composable private fun MidnightCard( - card: Card, cardBg: Color, isWild: Boolean, + card: Card, cardBg: Color, isWild: Boolean, w: Dp, h: Dp, off: Dp, modifier: Modifier, selected: Boolean, playable: Boolean, onClick: () -> Unit, onLongPress: () -> Unit ) { val glow = if (isWild) Color.Magenta.copy(alpha = 0.6f) else cardBg.copy(alpha = 0.6f) Box( modifier = modifier - .width(60.dp).height(90.dp) - .then(if (selected) Modifier.offset(y = (-30).dp) else Modifier) + .width(w).height(h) + .then(if (selected) Modifier.offset(y = off) else Modifier) .shadow(if (selected) 12.dp else 4.dp, RoundedCornerShape(12.dp), ambientColor = glow, spotColor = glow) .clip(RoundedCornerShape(12.dp)) .background(DarkCard) @@ -153,14 +158,14 @@ private fun MidnightCard( @OptIn(ExperimentalFoundationApi::class) @Composable private fun NeonCard( - card: Card, cardBg: Color, isWild: Boolean, + card: Card, cardBg: Color, isWild: Boolean, w: Dp, h: Dp, off: Dp, 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 = (-30).dp) else Modifier) + .width(w).height(h) + .then(if (selected) Modifier.offset(y = off) else Modifier) .shadow(if (selected) 10.dp else 4.dp, RoundedCornerShape(8.dp), ambientColor = neonColor, spotColor = neonColor) .clip(RoundedCornerShape(8.dp)) .background(Color(0xFF0A0A0A)) @@ -182,13 +187,13 @@ private fun NeonCard( @OptIn(ExperimentalFoundationApi::class) @Composable private fun PastelCard( - card: Card, cardBg: Color, isWild: Boolean, + card: Card, cardBg: Color, isWild: Boolean, w: Dp, h: Dp, off: Dp, 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 = (-30).dp) else Modifier) + .width(w).height(h) + .then(if (selected) Modifier.offset(y = off) else Modifier) .shadow(if (selected) 6.dp else 2.dp, RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp)) .background( @@ -218,13 +223,13 @@ private fun PastelCard( @OptIn(ExperimentalFoundationApi::class) @Composable private fun ForestCard( - card: Card, cardBg: Color, isWild: Boolean, + card: Card, cardBg: Color, isWild: Boolean, w: Dp, h: Dp, off: Dp, 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 = (-30).dp) else Modifier) + .width(w).height(h) + .then(if (selected) Modifier.offset(y = off) else Modifier) .shadow(if (selected) 6.dp else 2.dp, RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp)) .background( @@ -243,13 +248,13 @@ private fun ForestCard( @OptIn(ExperimentalFoundationApi::class) @Composable private fun OceanCard( - card: Card, cardBg: Color, isWild: Boolean, + card: Card, cardBg: Color, isWild: Boolean, w: Dp, h: Dp, off: Dp, 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 = (-30).dp) else Modifier) + .width(w).height(h) + .then(if (selected) Modifier.offset(y = off) else Modifier) .shadow(if (selected) 6.dp else 3.dp, RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp)) .background( @@ -351,11 +356,13 @@ private fun CardContent(card: Card, isWild: Boolean) { } @Composable -fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.current) { +fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.current, scale: Float = LocalCardScale.current) { + val w = (60 * scale).dp + val h = (90 * scale).dp when (theme) { CardTheme.CLASSIC -> { Box( - modifier = modifier.width(60.dp).height(90.dp) + modifier = modifier.width(w).height(h) .shadow(2.dp, RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp)) .background(DarkCard) @@ -371,7 +378,7 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu } CardTheme.ELEGANT -> { Box( - modifier = modifier.width(60.dp).height(90.dp) + modifier = modifier.width(w).height(h) .shadow(3.dp, RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp)) .background(Color.White) @@ -386,7 +393,7 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu } CardTheme.MIDNIGHT -> { Box( - modifier = modifier.width(60.dp).height(90.dp) + modifier = modifier.width(w).height(h) .shadow(4.dp, RoundedCornerShape(12.dp), ambientColor = Color.Magenta.copy(alpha = 0.4f)) .clip(RoundedCornerShape(12.dp)) .background(DarkCard) @@ -396,7 +403,7 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu } CardTheme.NEON -> { Box( - modifier = modifier.width(60.dp).height(90.dp) + modifier = modifier.width(w).height(h) .shadow(4.dp, RoundedCornerShape(8.dp), ambientColor = Color.Cyan) .clip(RoundedCornerShape(8.dp)) .background(Color(0xFF0A0A0A)) @@ -406,7 +413,7 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu } CardTheme.PASTEL -> { Box( - modifier = modifier.width(60.dp).height(90.dp) + modifier = modifier.width(w).height(h) .shadow(2.dp, RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp)) .background(Brush.verticalGradient(listOf(Color(0xFFFFF3E0), Color(0xFFFCE4EC)))) @@ -416,7 +423,7 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu } CardTheme.FOREST -> { Box( - modifier = modifier.width(60.dp).height(90.dp) + modifier = modifier.width(w).height(h) .shadow(2.dp, RoundedCornerShape(6.dp)) .clip(RoundedCornerShape(6.dp)) .background(Brush.verticalGradient(listOf(Color(0xFF2E7D32), Color(0xFF1B5E20)))) @@ -426,7 +433,7 @@ fun CardBack(modifier: Modifier = Modifier, theme: CardTheme = LocalCardTheme.cu } CardTheme.OCEAN -> { Box( - modifier = modifier.width(60.dp).height(90.dp) + modifier = modifier.width(w).height(h) .shadow(3.dp, RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp)) .background(Brush.verticalGradient(listOf(Color(0xFF0277BD), Color(0xFF00BCD4)))) diff --git a/app/src/main/java/com/unogame/ui/components/PlayerHand.kt b/app/src/main/java/com/unogame/ui/components/PlayerHand.kt index 6b1c8cd..867a57d 100644 --- a/app/src/main/java/com/unogame/ui/components/PlayerHand.kt +++ b/app/src/main/java/com/unogame/ui/components/PlayerHand.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.unogame.model.Card import com.unogame.model.CardColor +import com.unogame.ui.theme.LocalCardScale @Composable fun PlayerHand( @@ -25,6 +26,7 @@ fun PlayerHand( modifier: Modifier = Modifier ) { val scrollState = rememberScrollState() + val cardScale = LocalCardScale.current Column(modifier = modifier) { Text( @@ -45,6 +47,7 @@ fun PlayerHand( card = card, selected = index == selectedIndex, playable = isPlayable, + scale = cardScale, onClick = { onCardClick(index) }, modifier = Modifier.padding(horizontal = 2.dp) ) diff --git a/app/src/main/java/com/unogame/ui/screens/GameScreen.kt b/app/src/main/java/com/unogame/ui/screens/GameScreen.kt index eafd31f..7eee230 100644 --- a/app/src/main/java/com/unogame/ui/screens/GameScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/GameScreen.kt @@ -37,6 +37,7 @@ fun GameScreen( onShowLog: () -> Unit = {}, onPlaySeven: (Int) -> Unit = {}, isSevenZeroMode: Boolean = false, + handOffset: Float = 0f, errorMessage: String ) { val scrollState = rememberScrollState() @@ -44,6 +45,8 @@ fun GameScreen( var showColorPicker by remember { mutableStateOf(false) } var selectedAutoCard by remember { mutableIntStateOf(-1) } val flipped = gameState.flipped + val cardScale = LocalCardScale.current + val centerScale = 1.8f // 弃牌堆/摸牌堆较大的尺寸 val topCard = gameState.topCard val currentPlayer = gameState.currentPlayer @@ -63,7 +66,7 @@ fun GameScreen( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) - .padding(top = 24.dp, bottom = 100.dp) + .padding(top = 16.dp, bottom = (100f - handOffset).coerceAtLeast(0f).dp) ) { // 出牌状态条,最多2行 Box(modifier = Modifier.fillMaxWidth().heightIn(min = 36.dp, max = 56.dp)) { @@ -144,7 +147,7 @@ fun GameScreen( modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) } // Other players @@ -176,7 +179,7 @@ fun GameScreen( } } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(6.dp)) // Center area: discard pile + draw pile Row( @@ -189,6 +192,7 @@ fun GameScreen( // Draw pile Column(horizontalAlignment = Alignment.CenterHorizontally) { CardBack( + scale = centerScale, modifier = Modifier.clickable(enabled = isMyTurn) { onDrawCard() } @@ -216,6 +220,7 @@ fun GameScreen( color = if (gameState.currentWildColor != null && topCard.color == CardColor.WILD) gameState.currentWildColor!! else displayTop.color ), + scale = centerScale, playable = false ) } @@ -325,6 +330,7 @@ fun GameScreen( }, modifier = Modifier .align(Alignment.BottomCenter) + .offset(y = (-handOffset).dp) .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 12.dp) .background(LocalTableBg.current.color.copy(alpha = 0.9f)) diff --git a/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt b/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt index 5c9ff5a..1a9c270 100644 --- a/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/LocalGameScreen.kt @@ -121,6 +121,7 @@ fun LocalGameScreen( humanPlayerName: String, mode: GameMode, botNames: List = emptyList(), + handOffset: Float = 0f, onBackToMenu: () -> Unit ) { val scope = rememberCoroutineScope() @@ -600,6 +601,7 @@ fun LocalGameScreen( myPlayerId = myPlayerId, isMyTurn = isMyTurn, errorMessage = errorMessage, + handOffset = handOffset, isSevenZeroMode = mode == GameMode.SEVEN_ZERO, onPlayCard = { index -> executePlay(index, selectedWildColor) }, onPlaySeven = { index -> executePlay(index, null) }, diff --git a/app/src/main/java/com/unogame/ui/screens/SettingsScreen.kt b/app/src/main/java/com/unogame/ui/screens/SettingsScreen.kt index 84b2e07..b5c1957 100644 --- a/app/src/main/java/com/unogame/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/unogame/ui/screens/SettingsScreen.kt @@ -41,9 +41,11 @@ fun SettingsScreen( currentTheme: CardTheme, currentBg: TableBg, isLandscape: Boolean, + handOffset: Float, onNameChanged: (String) -> Unit, onSetTheme: (CardTheme) -> Unit, onSetBg: (TableBg) -> Unit, + onSetHandOffset: (Float) -> Unit, onToggleOrientation: () -> Unit, onBack: () -> Unit ) { @@ -237,6 +239,37 @@ fun SettingsScreen( onClick = onToggleOrientation ) + Spacer(modifier = Modifier.height(12.dp)) + + // 手牌区高度调整 + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors(containerColor = DarkSurface) + ) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.UnfoldLess, null, tint = Color.White.copy(alpha = 0.6f), modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.width(12.dp)) + Text("手牌区高度", color = Color.White, fontSize = 15.sp, modifier = Modifier.weight(1f)) + Text("${handOffset.toInt()}dp", color = GoldAccent, fontSize = 14.sp) + } + Spacer(modifier = Modifier.height(8.dp)) + Slider( + value = handOffset, + onValueChange = { onSetHandOffset(it) }, + valueRange = 0f..240f, + steps = 23, + modifier = Modifier.fillMaxWidth(), + colors = SliderDefaults.colors( + thumbColor = GoldAccent, + activeTrackColor = GoldAccent + ) + ) + Text("贴底 ← → 抬高", color = Color.White.copy(alpha = 0.35f), fontSize = 11.sp) + } + } + Spacer(modifier = Modifier.height(28.dp)) // About diff --git a/app/src/main/java/com/unogame/ui/theme/Theme.kt b/app/src/main/java/com/unogame/ui/theme/Theme.kt index 9479688..7c97359 100644 --- a/app/src/main/java/com/unogame/ui/theme/Theme.kt +++ b/app/src/main/java/com/unogame/ui/theme/Theme.kt @@ -1,6 +1,30 @@ package com.unogame.ui.theme +import android.content.Context import androidx.compose.runtime.compositionLocalOf val LocalCardTheme = compositionLocalOf { CardTheme.ELEGANT } val LocalTableBg = compositionLocalOf { TableBg.GREEN } +val LocalCardScale = compositionLocalOf { 1.0f } + +// 手牌缩放:0.5 ~ 1.3,默认 1.0(原始大小) +fun loadCardScale(context: Context): Float { + return context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE) + .getFloat("card_scale", 1.0f) +} + +fun saveCardScale(context: Context, scale: Float) { + context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE) + .edit().putFloat("card_scale", scale).apply() +} + +// 手牌区高度偏移(dp),越大手牌越靠上。默认 160,范围 0~240 +fun loadHandOffset(context: Context): Float { + return context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE) + .getFloat("hand_offset", 160f) +} + +fun saveHandOffset(context: Context, offset: Float) { + context.getSharedPreferences("unogame_prefs", Context.MODE_PRIVATE) + .edit().putFloat("hand_offset", offset).apply() +}