Tugas 10 - ScrambleWord
Nama : Widian Sasi Disertasiani
NRP : 5025211024
Kelas : PPB D
Materi : Membuat Aplikasi ScrambleWord
Github: Code
Yt: Demo
Code:
package com.example.unscramble.ui
import android.app.Activity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.unscramble.R
import com.example.unscramble.ui.theme.UnscrambleTheme
import kotlinx.coroutines.delay
@Composable
fun GameScreen(gameViewModel: GameViewModel = viewModel()) {
val gameUiState by gameViewModel.uiState.collectAsState()
val mediumPadding = dimensionResource(R.dimen.padding_medium)
// Animation states
var showSuccessAnimation by remember { mutableStateOf(false) }
var showErrorAnimation by remember { mutableStateOf(false) }
val infiniteTransition = rememberInfiniteTransition(label = "sparkle")
val sparkleRotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
), label = "sparkle_rotation"
)
Box(
modifier = Modifier
.background(
brush = Brush.verticalGradient(
colors = listOf(
colorScheme.primaryContainer.copy(alpha = 0.3f),
colorScheme.surface
)
)
)
) {
Column(
modifier = Modifier
.statusBarsPadding()
.verticalScroll(rememberScrollState())
.safeDrawingPadding()
.padding(mediumPadding),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Enhanced App Title with animation
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 16.dp)
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = colorScheme.primary,
modifier = Modifier
.size(32.dp)
.rotate(sparkleRotation)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.app_name),
style = typography.headlineLarge.copy(
fontWeight = FontWeight.Bold,
color = colorScheme.primary
),
)
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = colorScheme.primary,
modifier = Modifier
.size(32.dp)
.rotate(-sparkleRotation)
)
}
// Hearts Display - Using a default value since heartsRemaining doesn't exist
HeartsDisplay(3)
Spacer(modifier = Modifier.height(16.dp))
GameLayout(
onUserGuessChanged = { gameViewModel.updateUserGuess(it) },
wordCount = gameUiState.currentWordCount,
userGuess = gameViewModel.userGuess,
onKeyboardDone = {
gameViewModel.checkUserGuess()
},
currentScrambledWord = gameUiState.currentScrambledWord,
isGuessWrong = gameUiState.isGuessedWordWrong,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(mediumPadding)
)
// Enhanced Action Buttons
EnhancedActionButtons(
onSubmit = { gameViewModel.checkUserGuess() },
onSkip = { gameViewModel.skipWord() },
onShuffle = {
// Add shuffle functionality or remove if not implemented
// gameViewModel.shuffleCurrentWord()
},
modifier = Modifier
.fillMaxWidth()
.padding(mediumPadding)
)
// Enhanced Game Status - Using default value since currentStreak doesn't exist
EnhancedGameStatus(
score = gameUiState.score,
streak = 0, // Default value since currentStreak doesn't exist
modifier = Modifier.padding(20.dp)
)
if (gameUiState.isGameOver) {
EnhancedFinalScoreDialog(
score = gameUiState.score,
wordsGuessed = gameUiState.currentWordCount - 1,
onPlayAgain = { gameViewModel.resetGame() }
)
}
}
// Success Animation Overlay
AnimatedVisibility(
visible = showSuccessAnimation,
enter = scaleIn() + fadeIn(),
exit = scaleOut() + fadeOut()
) {
SuccessOverlay()
}
}
}
@Composable
fun HeartsDisplay(heartsRemaining: Int) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
repeat(3) { index ->
val heartScale = remember { Animatable(1f) }
LaunchedEffect(heartsRemaining) {
if (index >= heartsRemaining && index < 3) {
heartScale.animateTo(
targetValue = 0f,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
}
}
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "Heart ${index + 1}",
tint = if (index < heartsRemaining) Color.Red else Color.Gray,
modifier = Modifier
.size(28.dp)
.scale(heartScale.value)
.padding(horizontal = 4.dp)
)
}
}
}
@Composable
fun EnhancedActionButtons(
onSubmit: () -> Unit,
onSkip: () -> Unit,
onShuffle: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Submit Button with gradient-like effect
Button(
modifier = Modifier.fillMaxWidth(),
onClick = onSubmit,
colors = ButtonDefaults.buttonColors(
containerColor = colorScheme.primary
),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 6.dp,
pressedElevation = 2.dp
)
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.submit),
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
// Secondary Action Buttons Row
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Shuffle Button
OutlinedButton(
onClick = onShuffle,
modifier = Modifier.weight(1f),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = colorScheme.secondary
)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Shuffle",
fontSize = 14.sp
)
}
// Skip Button
OutlinedButton(
onClick = onSkip,
modifier = Modifier.weight(1f)
) {
Text(
text = stringResource(R.string.skip),
fontSize = 14.sp
)
}
}
}
}
@Composable
fun EnhancedGameStatus(
score: Int,
streak: Int,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier,
colors = CardDefaults.cardColors(
containerColor = colorScheme.primaryContainer.copy(alpha = 0.5f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Score
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Score",
style = typography.labelMedium,
color = colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
)
Text(
text = score.toString(),
style = typography.headlineMedium.copy(
fontWeight = FontWeight.Bold,
color = colorScheme.primary
)
)
}
// Divider
Box(
modifier = Modifier
.width(1.dp)
.height(50.dp)
.background(colorScheme.outline.copy(alpha = 0.3f))
)
// Streak
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Streak",
style = typography.labelMedium,
color = colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
if (streak > 0) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = Color(0xFFFFD700), // Gold color
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(4.dp))
}
Text(
text = streak.toString(),
style = typography.headlineMedium.copy(
fontWeight = FontWeight.Bold,
color = if (streak > 0) Color(0xFFFFD700) else colorScheme.primary
)
)
}
}
}
}
}
@Composable
fun GameStatus(score: Int, modifier: Modifier = Modifier) {
Card(
modifier = modifier
) {
Text(
text = stringResource(R.string.score, score),
style = typography.headlineMedium,
modifier = Modifier.padding(8.dp)
)
}
}
@Composable
fun GameLayout(
currentScrambledWord: String,
wordCount: Int,
isGuessWrong: Boolean,
userGuess: String,
onUserGuessChanged: (String) -> Unit,
onKeyboardDone: () -> Unit,
modifier: Modifier = Modifier
) {
val mediumPadding = dimensionResource(R.dimen.padding_medium)
val shakeOffset = remember { Animatable(0f) }
// Shake animation when guess is wrong
LaunchedEffect(isGuessWrong) {
if (isGuessWrong) {
repeat(5) {
shakeOffset.animateTo(
targetValue = 10f,
animationSpec = tween(50)
)
shakeOffset.animateTo(
targetValue = -10f,
animationSpec = tween(50)
)
}
shakeOffset.animateTo(
targetValue = 0f,
animationSpec = tween(50)
)
}
}
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(
containerColor = colorScheme.surface
)
) {
Column(
verticalArrangement = Arrangement.spacedBy(mediumPadding),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(mediumPadding)
) {
// Word Count Badge
Box(
modifier = Modifier
.align(alignment = Alignment.End)
.clip(shapes.medium)
.background(
brush = Brush.horizontalGradient(
colors = listOf(
colorScheme.primary,
colorScheme.primary.copy(alpha = 0.8f)
)
)
)
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Text(
text = stringResource(R.string.word_count, wordCount),
style = typography.labelLarge.copy(
fontWeight = FontWeight.SemiBold,
color = colorScheme.onPrimary
)
)
}
Spacer(modifier = Modifier.height(8.dp))
// Scrambled Word with individual letter animations
ScrambledWordDisplay(
scrambledWord = currentScrambledWord,
isError = isGuessWrong,
modifier = Modifier.scale(if (isGuessWrong) 0.95f else 1f)
)
Text(
text = stringResource(R.string.instructions),
textAlign = TextAlign.Center,
style = typography.bodyLarge.copy(
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
)
// Enhanced Input Field
OutlinedTextField(
value = userGuess,
singleLine = true,
shape = shapes.large,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
colors = TextFieldDefaults.colors(
focusedContainerColor = colorScheme.surface,
unfocusedContainerColor = colorScheme.surface,
disabledContainerColor = colorScheme.surface,
errorContainerColor = colorScheme.errorContainer.copy(alpha = 0.1f)
),
onValueChange = onUserGuessChanged,
label = {
Text(
text = if (isGuessWrong) {
stringResource(R.string.wrong_guess)
} else {
stringResource(R.string.enter_your_word)
},
style = typography.bodyMedium
)
},
isError = isGuessWrong,
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { onKeyboardDone() }
)
)
}
}
}
@Composable
fun ScrambledWordDisplay(
scrambledWord: String,
isError: Boolean,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
scrambledWord.forEachIndexed { index, char ->
LetterBox(
letter = char,
isError = isError,
animationDelay = index * 100L
)
}
}
}
@Composable
fun LetterBox(
letter: Char,
isError: Boolean,
animationDelay: Long
) {
val scale = remember { Animatable(0f) }
LaunchedEffect(letter) {
delay(animationDelay)
scale.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = 300,
easing = FastOutSlowInEasing
)
)
}
Box(
modifier = Modifier
.size(50.dp)
.scale(scale.value)
.clip(shapes.medium)
.background(
color = if (isError) {
colorScheme.errorContainer.copy(alpha = 0.3f)
} else {
colorScheme.primaryContainer.copy(alpha = 0.5f)
}
)
.border(
width = 2.dp,
color = if (isError) {
colorScheme.error.copy(alpha = 0.5f)
} else {
colorScheme.primary.copy(alpha = 0.3f)
},
shape = shapes.medium
)
.clickable { /* Could add letter selection functionality */ },
contentAlignment = Alignment.Center
) {
Text(
text = letter.uppercase(),
style = typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
color = if (isError) {
colorScheme.error
} else {
colorScheme.primary
}
)
)
}
}
@Composable
fun SuccessOverlay() {
Box(
modifier = Modifier
.background(
color = colorScheme.primary.copy(alpha = 0.1f)
),
contentAlignment = Alignment.Center
) {
Card(
colors = CardDefaults.cardColors(
containerColor = colorScheme.primaryContainer
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(24.dp)
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = Color(0xFFFFD700),
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Excellent!",
style = typography.headlineMedium.copy(
fontWeight = FontWeight.Bold,
color = colorScheme.primary
)
)
}
}
}
}
@Composable
private fun EnhancedFinalScoreDialog(
score: Int,
wordsGuessed: Int,
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = (LocalContext.current as Activity)
AlertDialog(
onDismissRequest = { },
title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = Color(0xFFFFD700),
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.congratulations),
style = typography.headlineMedium.copy(
fontWeight = FontWeight.Bold
)
)
}
},
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.you_scored, score),
style = typography.bodyLarge,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Words guessed: $wordsGuessed",
style = typography.bodyMedium.copy(
color = colorScheme.onSurface.copy(alpha = 0.7f)
),
textAlign = TextAlign.Center
)
}
},
modifier = modifier,
dismissButton = {
TextButton(
onClick = { activity.finish() }
) {
Text(text = stringResource(R.string.exit))
}
},
confirmButton = {
Button(
onClick = onPlayAgain,
elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp)
) {
Text(text = stringResource(R.string.play_again))
}
}
)
}
/*
* Creates and shows an AlertDialog with final score.
*/
@Composable
private fun FinalScoreDialog(
score: Int,
onPlayAgain: () -> Unit,
modifier: Modifier = Modifier
) {
val activity = (LocalContext.current as Activity)
AlertDialog(
onDismissRequest = {
// Dismiss the dialog when the user clicks outside the dialog or on the back
// button. If you want to disable that functionality, simply use an empty
// onCloseRequest.
},
title = { Text(text = stringResource(R.string.congratulations)) },
text = { Text(text = stringResource(R.string.you_scored, score)) },
modifier = modifier,
dismissButton = {
TextButton(
onClick = {
activity.finish()
}
) {
Text(text = stringResource(R.string.exit))
}
},
confirmButton = {
TextButton(onClick = onPlayAgain) {
Text(text = stringResource(R.string.play_again))
}
}
)
}
@Preview(showBackground = true)
@Composable
fun GameScreenPreview() {
UnscrambleTheme {
GameScreen()
}
}
Komentar
Posting Komentar