Tugas 9 - Dessert Clicker
Nama : Widian Sasi Disertasiani
NRP : 5025211024
Kelas : PPB D
Materi : Membuat aplikasi dessert clicker
Github: Code
Yt: Demo
Code:
package com.example.dessertclicker
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
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.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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.core.content.ContextCompat
import com.example.dessertclicker.data.Datasource
import com.example.dessertclicker.model.Dessert
import com.example.dessertclicker.ui.theme.DessertClickerTheme
import kotlinx.coroutines.delay
import kotlin.random.Random
// Tag for logging
private const val TAG = "MainActivity"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate Called")
setContent {
DessertClickerTheme {
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding(),
) {
DessertClickerApp(desserts = Datasource.dessertList)
}
}
}
}
override fun onStart() {
super.onStart()
Log.d(TAG, "onStart Called")
}
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume Called")
}
override fun onRestart() {
super.onRestart()
Log.d(TAG, "onRestart Called")
}
override fun onPause() {
super.onPause()
Log.d(TAG, "onPause Called")
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onStop Called")
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy Called")
}
}
/**
* Determine which dessert to show.
*/
fun determineDessertToShow(
desserts: List<Dessert>,
dessertsSold: Int
): Dessert {
var dessertToShow = desserts.first()
for (dessert in desserts) {
if (dessertsSold >= dessert.startProductionAmount) {
dessertToShow = dessert
} else {
break
}
}
return dessertToShow
}
/**
* Share desserts sold information using ACTION_SEND intent
*/
private fun shareSoldDessertsInformation(intentContext: Context, dessertsSold: Int, revenue: Int) {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(
Intent.EXTRA_TEXT,
intentContext.getString(R.string.share_text, dessertsSold, revenue)
)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
try {
ContextCompat.startActivity(intentContext, shareIntent, null)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
intentContext,
intentContext.getString(R.string.sharing_not_available),
Toast.LENGTH_LONG
).show()
}
}
@Composable
private fun DessertClickerApp(
desserts: List<Dessert>
) {
var revenue by rememberSaveable { mutableStateOf(0) }
var dessertsSold by rememberSaveable { mutableStateOf(0) }
val currentDessertIndex by rememberSaveable { mutableStateOf(0) }
var currentDessertPrice by rememberSaveable {
mutableStateOf(desserts[currentDessertIndex].price)
}
var currentDessertImageId by rememberSaveable {
mutableStateOf(desserts[currentDessertIndex].imageId)
}
// Animation states
var showCelebration by remember { mutableStateOf(false) }
// Launch celebration effect when milestones are reached
LaunchedEffect(dessertsSold) {
if (dessertsSold > 0 && dessertsSold % 10 == 0) {
showCelebration = true
delay(2000)
showCelebration = false
}
}
Box(modifier = Modifier.fillMaxSize()) {
// Animated gradient background
AnimatedGradientBackground()
Scaffold(
containerColor = Color.Transparent,
topBar = {
val intentContext = LocalContext.current
val layoutDirection = LocalLayoutDirection.current
EnhancedDessertClickerAppBar(
onShareButtonClicked = {
shareSoldDessertsInformation(
intentContext = intentContext,
dessertsSold = dessertsSold,
revenue = revenue
)
},
modifier = Modifier
.fillMaxWidth()
.padding(
start = WindowInsets.safeDrawing.asPaddingValues()
.calculateStartPadding(layoutDirection),
end = WindowInsets.safeDrawing.asPaddingValues()
.calculateEndPadding(layoutDirection),
)
)
}
) { contentPadding ->
EnhancedDessertClickerScreen(
revenue = revenue,
dessertsSold = dessertsSold,
dessertImageId = currentDessertImageId,
onDessertClicked = {
// Update the revenue
revenue += currentDessertPrice
dessertsSold++
// Show the next dessert
val dessertToShow = determineDessertToShow(desserts, dessertsSold)
currentDessertImageId = dessertToShow.imageId
currentDessertPrice = dessertToShow.price
},
modifier = Modifier.padding(contentPadding)
)
}
// Celebration overlay
if (showCelebration) {
CelebrationOverlay()
}
// Floating particles
FloatingParticles()
}
}
@Composable
private fun AnimatedGradientBackground() {
val infiniteTransition = rememberInfiniteTransition(label = "gradient")
val color1 by infiniteTransition.animateColor(
initialValue = Color(0xFFFF6B9D),
targetValue = Color(0xFF4ECDC4),
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "color1"
)
val color2 by infiniteTransition.animateColor(
initialValue = Color(0xFF4ECDC4),
targetValue = Color(0xFFFFE66D),
animationSpec = infiniteRepeatable(
animation = tween(3000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "color2"
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
Brush.radialGradient(
colors = listOf(color1, color2, Color(0xFFFF6B9D)),
radius = 1000f
)
)
)
}
@Composable
private fun EnhancedDessertClickerAppBar(
onShareButtonClicked: () -> Unit,
modifier: Modifier = Modifier
) {
val sparkleRotation by rememberInfiniteTransition(label = "sparkle").animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(2000, easing = LinearEasing)
)
)
Card(
modifier = modifier.padding(16.dp),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White.copy(alpha = 0.9f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = null,
tint = Color(0xFFFF6B9D),
modifier = Modifier
.size(32.dp)
.rotate(sparkleRotation)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "🍰 Sweet Clicker",
color = Color(0xFF2D3436),
style = MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold
)
)
}
Button(
onClick = onShareButtonClicked,
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF6B9D)
),
shape = CircleShape,
modifier = Modifier.size(48.dp),
contentPadding = PaddingValues(0.dp)
) {
Icon(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.share),
tint = Color.White
)
}
}
}
}
@Composable
fun EnhancedDessertClickerScreen(
revenue: Int,
dessertsSold: Int,
@DrawableRes dessertImageId: Int,
onDessertClicked: () -> Unit,
modifier: Modifier = Modifier
) {
var isPressed by remember { mutableStateOf(false) }
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.9f else 1f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "scale"
)
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(32.dp))
// Dessert clicker area with enhanced animations
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
// Glow effect behind dessert
Box(
modifier = Modifier
.size(220.dp)
.background(
Color(0xFFFFE66D).copy(alpha = 0.3f),
CircleShape
)
.scale(scale * 1.1f)
)
// Main dessert image
Card(
modifier = Modifier
.size(200.dp)
.scale(scale)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
isPressed = true
onDessertClicked()
}
.shadow(
elevation = 16.dp,
shape = CircleShape,
ambientColor = Color(0xFFFF6B9D),
spotColor = Color(0xFFFF6B9D)
),
shape = CircleShape,
colors = CardDefaults.cardColors(
containerColor = Color.White
)
) {
Image(
painter = painterResource(dessertImageId),
contentDescription = null,
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop,
)
}
// Click effect
LaunchedEffect(isPressed) {
if (isPressed) {
delay(100)
isPressed = false
}
}
}
// Enhanced transaction info
EnhancedTransactionInfo(
revenue = revenue,
dessertsSold = dessertsSold,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun EnhancedTransactionInfo(
revenue: Int,
dessertsSold: Int,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.padding(16.dp),
shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White.copy(alpha = 0.95f)
),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Decorative top bar
Box(
modifier = Modifier
.width(50.dp)
.height(4.dp)
.background(
Color(0xFFDDD6FE),
RoundedCornerShape(2.dp)
)
)
Spacer(modifier = Modifier.height(16.dp))
// Stats cards
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
StatCard(
icon = Icons.Filled.Add,
title = "Desserts Sold",
value = dessertsSold.toString(),
color = Color(0xFF4ECDC4),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(16.dp))
StatCard(
icon = Icons.Filled.Star,
title = "Total Revenue",
value = "$$revenue",
color = Color(0xFFFFE66D),
modifier = Modifier.weight(1f)
)
}
Spacer(modifier = Modifier.height(16.dp))
// Progress indicator
if (dessertsSold > 0) {
val progress = (dessertsSold % 10) / 10f
ProgressIndicator(
progress = progress,
nextMilestone = ((dessertsSold / 10) + 1) * 10
)
}
}
}
}
@Composable
private fun StatCard(
icon: androidx.compose.ui.graphics.vector.ImageVector,
title: String,
value: String,
color: Color,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier,
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = color.copy(alpha = 0.1f)
),
border = BorderStroke(2.dp, color.copy(alpha = 0.3f))
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color,
modifier = Modifier.size(32.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = title,
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF636E72),
textAlign = TextAlign.Center
)
Text(
text = value,
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.Bold
),
color = color,
textAlign = TextAlign.Center
)
}
}
}
@Composable
private fun ProgressIndicator(
progress: Float,
nextMilestone: Int
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Next milestone:",
style = MaterialTheme.typography.labelMedium,
color = Color(0xFF636E72)
)
Text(
text = "$nextMilestone desserts",
style = MaterialTheme.typography.labelMedium.copy(
fontWeight = FontWeight.Bold
),
color = Color(0xFFFF6B9D)
)
}
Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(4.dp)),
color = Color(0xFFFF6B9D),
trackColor = Color(0xFFFF6B9D).copy(alpha = 0.2f)
)
}
}
@Composable
private fun CelebrationOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f)),
contentAlignment = Alignment.Center
) {
Card(
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = Color.White
),
elevation = CardDefaults.cardElevation(defaultElevation = 16.dp)
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "🎉",
fontSize = 64.sp
)
Text(
text = "Milestone Reached!",
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.Bold
),
color = Color(0xFFFF6B9D)
)
Text(
text = "Keep clicking those desserts!",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF636E72)
)
}
}
}
}
@Composable
private fun FloatingParticles() {
Box(modifier = Modifier.fillMaxSize()) {
repeat(5) { index ->
FloatingParticle(
modifier = Modifier.offset(
x = (50 + index * 60).dp,
y = (100 + index * 80).dp
)
)
}
}
}
@Composable
private fun FloatingParticle(modifier: Modifier = Modifier) {
val infiniteTransition = rememberInfiniteTransition(label = "particle")
val offsetY by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = -20f,
animationSpec = infiniteRepeatable(
animation = tween(2000 + Random.nextInt(1000), easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
val alpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 0.8f,
animationSpec = infiniteRepeatable(
animation = tween(1500, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
Text(
text = listOf("✨", "🌟", "💫", "⭐", "🎈").random(),
fontSize = 16.sp,
modifier = modifier
.graphicsLayer {
translationY = offsetY
this.alpha = alpha
}
)
}
@Preview
@Composable
fun MyDessertClickerAppPreview() {
DessertClickerTheme {
DessertClickerApp(listOf(Dessert(R.drawable.cupcake, 5, 0)))
}
}
Komentar
Posting Komentar