feat: 弃牌堆/摸牌堆1.8倍放大,手牌区高度可调0~240dp默认160dp

This commit is contained in:
flykhan 2026-04-26 22:19:28 +08:00
parent 570d96c5c0
commit 807976ca51
7 changed files with 124 additions and 40 deletions

View File

@ -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<NavBackStackEntry>.() -> 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()

View File

@ -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))))

View File

@ -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)
)

View File

@ -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))

View File

@ -121,6 +121,7 @@ fun LocalGameScreen(
humanPlayerName: String,
mode: GameMode,
botNames: List<String> = 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) },

View File

@ -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

View File

@ -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()
}