diff --git a/app/src/main/java/com/example/tiptime/BmiLogic.kt b/app/src/main/java/com/example/tiptime/BmiLogic.kt new file mode 100644 index 0000000..131c5ec --- /dev/null +++ b/app/src/main/java/com/example/tiptime/BmiLogic.kt @@ -0,0 +1,72 @@ +package com.example.tiptime + +import kotlin.math.pow + +// File ini berisi semua logika bisnis dan kalkulasi untuk aplikasi BMI. + +/** + * Data class untuk menampung semua hasil kalkulasi BMI. + */ +data class BmiFullResult( + val bmi: Double, + val category: String, + val healthyBmiRange: String = "18.5 - 25.0", + val healthyWeightRange: String, + val bmiPrime: Double, + val ponderalIndex: Double +) + +/** + * Menghitung semua nilai terkait BMI berdasarkan berat, tinggi, dan sistem unit. + * @param weight Berat badan pengguna (dalam kg atau lbs). + * @param height Tinggi badan pengguna (dalam cm atau in). + * @param useMetric `true` jika menggunakan sistem Metrik, `false` jika USC. + * @return `BmiFullResult` jika input valid, atau `null` jika tidak. + */ +fun calculateBmi(weight: Double, height: Double, useMetric: Boolean): BmiFullResult? { + if (height <= 0 || weight <= 0) return null + + val bmi = if (useMetric) { + val heightInMeters = height / 100 + weight / heightInMeters.pow(2) + } else { + (weight * 703) / height.pow(2) + } + + val category = determineBmiCategory(bmi) + val heightInMeters = if (useMetric) height / 100 else height * 0.0254 + val minHealthyWeight = 18.5 * heightInMeters.pow(2) + val maxHealthyWeight = 25.0 * heightInMeters.pow(2) + + val healthyWeightRangeString = if (useMetric) { + String.format("%.1f kg - %.1f kg", minHealthyWeight, maxHealthyWeight) + } else { + val minHealthyWeightLbs = minHealthyWeight * 2.20462 + val maxHealthyWeightLbs = maxHealthyWeight * 2.20462 + String.format("%.1f lbs - %.1f lbs", minHealthyWeightLbs, maxHealthyWeightLbs) + } + + val bmiPrime = bmi / 25.0 + val weightInKg = if (useMetric) weight else weight * 0.453592 + val ponderalIndex = weightInKg / heightInMeters.pow(3) + + return BmiFullResult( + bmi = bmi, + category = category, + healthyWeightRange = healthyWeightRangeString, + bmiPrime = bmiPrime, + ponderalIndex = ponderalIndex + ) +} + +/** + * Menentukan kategori BMI berdasarkan nilainya. + */ +fun determineBmiCategory(bmi: Double): String { + return when { + bmi < 18.5 -> "Berat badan kurang" + bmi < 25 -> "Normal" + bmi < 30 -> "Berat badan lebih" + else -> "Obesitas" + } +} diff --git a/app/src/main/java/com/example/tiptime/MainActivity.kt b/app/src/main/java/com/example/tiptime/MainActivity.kt index b5cd9f8..13556c3 100644 --- a/app/src/main/java/com/example/tiptime/MainActivity.kt +++ b/app/src/main/java/com/example/tiptime/MainActivity.kt @@ -9,35 +9,39 @@ import androidx.activity.compose.setContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.DateRange -import androidx.compose.material.icons.filled.LineWeight -import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.MonitorWeight import androidx.compose.material.icons.filled.SwapVert 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.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +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 com.example.tiptime.ui.theme.TipTimeTheme import java.util.Locale +// --- PERBAIKAN DI SINI: Menambahkan kembali import yang hilang --- import kotlin.math.cos -import kotlin.math.pow import kotlin.math.sin class MainActivity : ComponentActivity() { @@ -53,15 +57,6 @@ class MainActivity : ComponentActivity() { } } -data class BmiFullResult( - val bmi: Double, - val category: String, - val healthyBmiRange: String = "18.5 - 25.0", - val healthyWeightRange: String, - val bmiPrime: Double, - val ponderalIndex: Double -) - @Composable fun BmiCalculatorScreen() { var heightInput by remember { mutableStateOf("") } @@ -76,8 +71,6 @@ fun BmiCalculatorScreen() { heightInput = "" weightInput = "" ageInput = "" - selectedGender = "Laki-laki" - useMetricUnits = true bmiResult = null validationError = null } @@ -89,15 +82,11 @@ fun BmiCalculatorScreen() { .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Kalkulator BMI", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) - Spacer(Modifier.height(8.dp)) - Text("by Hadi Prakoso", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary) - Spacer(Modifier.height(24.dp)) + Header() AnimatedVisibility(visible = bmiResult != null) { bmiResult?.let { ResultDisplay(it) } } - if (bmiResult != null) { Spacer(Modifier.height(24.dp)) } @@ -115,7 +104,8 @@ fun BmiCalculatorScreen() { text = it, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 8.dp) + modifier = Modifier.padding(top = 8.dp), + textAlign = TextAlign.Center ) } @@ -127,24 +117,11 @@ fun BmiCalculatorScreen() { val weight = weightInput.toDoubleOrNull() val age = ageInput.toIntOrNull() - // Input Validation when { - height == null || weight == null || age == null -> { - validationError = "Semua kolom (Umur, Tinggi, Berat) harus diisi dengan angka." - bmiResult = null - } - age <= 0 || age > 120 -> { - validationError = "Umur tidak wajar." - bmiResult = null - } - (useMetricUnits && (height <= 50 || height > 270)) || (!useMetricUnits && (height <= 20 || height > 108)) -> { - validationError = "Tinggi badan tidak wajar." - bmiResult = null - } - (useMetricUnits && (weight <= 20 || weight > 500)) || (!useMetricUnits && (weight <= 45 || weight > 1100)) -> { - validationError = "Berat badan tidak wajar." - bmiResult = null - } + height == null || weight == null || age == null -> validationError = "Semua kolom harus diisi dengan angka." + age <= 0 || age > 120 -> validationError = "Umur tidak wajar." + (useMetricUnits && (height <= 50 || height > 270)) || (!useMetricUnits && (height <= 20 || height > 108)) -> validationError = "Tinggi badan tidak wajar." + (useMetricUnits && (weight <= 20 || weight > 500)) || (!useMetricUnits && (weight <= 45 || weight > 1100)) -> validationError = "Berat badan tidak wajar." else -> { validationError = null bmiResult = calculateBmi(weight, height, useMetricUnits) @@ -156,6 +133,21 @@ fun BmiCalculatorScreen() { } } +@Composable +fun Header() { + Image( + painter = painterResource(id = R.drawable.ic_launcher_playstore), + contentDescription = "Logo Aplikasi", + modifier = Modifier + .size(80.dp) + .clip(CircleShape) + ) + Spacer(Modifier.height(16.dp)) + Text("Kalkulator BMI", style = MaterialTheme.typography.headlineLarge) + Text("by Hadi Prakoso", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary) + Spacer(Modifier.height(24.dp)) +} + @Composable fun ResultDisplay(result: BmiFullResult) { Card( @@ -164,13 +156,12 @@ fun ResultDisplay(result: BmiFullResult) { colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface) ) { Column(Modifier.padding(24.dp).fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Text("Hasil", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold) + Text("Hasil", style = MaterialTheme.typography.headlineSmall) Spacer(Modifier.height(16.dp)) BmiGauge(bmi = result.bmi) Spacer(Modifier.height(16.dp)) Text("BMI = ${String.format("%.1f", result.bmi)} kg/m² (${result.category})", style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary ) Spacer(Modifier.height(24.dp)) @@ -221,8 +212,13 @@ fun BmiGauge(bmi: Double) { drawCircle(surfaceColor, radius = 5.dp.toPx(), center = center) } - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = "BMI", style = MaterialTheme.typography.bodyLarge) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Bottom, + modifier = Modifier + .fillMaxSize() + .padding(bottom = 50.dp) + ) { Text( text = if (bmi > 0) String.format(Locale.getDefault(), "%.1f", bmi) else "-", style = MaterialTheme.typography.displayMedium.copy(fontWeight = FontWeight.Bold, color = onSurfaceColor) @@ -252,12 +248,18 @@ fun InputPanel( val heightLabel = if (useMetric) "Tinggi (cm)" else "Tinggi (in)" val weightLabel = if (useMetric) "Berat (kg)" else "Berat (lbs)" - Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) { - UnitToggle(useMetric = useMetric, onUnitChange = onUnitChange) - GenderSelector(selectedGender, onOptionSelected = { onGenderSelect(it) }) - BmiTextField(label = "Umur", value = ageInput, onValueChange = onAgeChange, icon = Icons.Default.DateRange) - BmiTextField(label = heightLabel, value = heightInput, onValueChange = onHeightChange, icon = Icons.Default.SwapVert) - BmiTextField(label = weightLabel, value = weightInput, onValueChange = onWeightChange, icon = Icons.Default.LineWeight) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) + ) { + Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + UnitToggle(useMetric = useMetric, onUnitChange = onUnitChange) + GenderSelector(selectedGender, onOptionSelected = { onGenderSelect(it) }) + BmiTextField(label = "Umur", value = ageInput, onValueChange = onAgeChange, icon = Icons.Default.DateRange) + BmiTextField(label = heightLabel, value = heightInput, onValueChange = onHeightChange, icon = Icons.Default.SwapVert) + BmiTextField(label = weightLabel, value = weightInput, onValueChange = onWeightChange, icon = Icons.Default.MonitorWeight) + } } } @@ -288,6 +290,8 @@ fun GenderSelector(selectedOption: String, onOptionSelected: (String) -> Unit) { val options = listOf("Laki-laki", "Perempuan") options.forEach { gender -> val isSelected = selectedOption == gender + val iconRes = if (gender == "Laki-laki") R.drawable.ic_male else R.drawable.ic_female + Button( onClick = { onOptionSelected(gender) }, shape = RoundedCornerShape(12.dp), @@ -297,7 +301,11 @@ fun GenderSelector(selectedOption: String, onOptionSelected: (String) -> Unit) { ), modifier = Modifier.weight(1f) ) { - Icon(Icons.Default.Person, contentDescription = gender, modifier = Modifier.size(18.dp)) + Icon( + painter = painterResource(id = iconRes), + contentDescription = gender, + modifier = Modifier.size(24.dp) + ) Spacer(Modifier.width(8.dp)) Text(gender) } @@ -318,52 +326,6 @@ fun BmiTextField(label: String, value: String, onValueChange: (String) -> Unit, ) } -// --- Calculation Logic --- -fun calculateBmi(weight: Double, height: Double, useMetric: Boolean): BmiFullResult? { - if (height <= 0 || weight <= 0) return null - - val bmi = if (useMetric) { - val heightInMeters = height / 100 - weight / heightInMeters.pow(2) - } else { - (weight * 703) / height.pow(2) - } - - val category = determineBmiCategory(bmi) - val heightInMeters = if (useMetric) height / 100 else height * 0.0254 - val minHealthyWeight = 18.5 * heightInMeters.pow(2) - val maxHealthyWeight = 25.0 * heightInMeters.pow(2) - - val healthyWeightRangeString = if (useMetric) { - String.format("%.1f kg - %.1f kg", minHealthyWeight, maxHealthyWeight) - } else { - val minHealthyWeightLbs = minHealthyWeight * 2.20462 - val maxHealthyWeightLbs = maxHealthyWeight * 2.20462 - String.format("%.1f lbs - %.1f lbs", minHealthyWeightLbs, maxHealthyWeightLbs) - } - - val bmiPrime = bmi / 25.0 - val weightInKg = if (useMetric) weight else weight * 0.453592 - val ponderalIndex = weightInKg / heightInMeters.pow(3) - - return BmiFullResult( - bmi = bmi, - category = category, - healthyWeightRange = healthyWeightRangeString, - bmiPrime = bmiPrime, - ponderalIndex = ponderalIndex - ) -} - -fun determineBmiCategory(bmi: Double): String { - return when { - bmi < 18.5 -> "Berat badan kurang" - bmi < 25 -> "Normal" - bmi < 30 -> "Berat badan lebih" - else -> "Obesitas" - } -} - @Preview(showBackground = true, widthDp = 360, heightDp = 800) @Composable fun BmiCalculatorScreenPreview() { diff --git a/app/src/main/java/com/example/tiptime/ui/theme/Type.kt b/app/src/main/java/com/example/tiptime/ui/theme/Type.kt index e23c87f..b73d1b4 100644 --- a/app/src/main/java/com/example/tiptime/ui/theme/Type.kt +++ b/app/src/main/java/com/example/tiptime/ui/theme/Type.kt @@ -5,32 +5,42 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -// Set of Material typography styles to start with +// Mengatur Typography default untuk menggunakan font Poppins val Typography = Typography( - // Override default text styles to use Poppins - headlineMedium = TextStyle( + headlineLarge = TextStyle( fontFamily = Poppins, fontWeight = FontWeight.Bold, - fontSize = 28.sp + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp ), - titleMedium = TextStyle( + titleLarge = TextStyle( fontFamily = Poppins, - fontWeight = FontWeight.Normal, - fontSize = 16.sp + fontWeight = FontWeight.Bold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp ), bodyLarge = TextStyle( fontFamily = Poppins, fontWeight = FontWeight.Normal, - fontSize = 16.sp + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp ), bodyMedium = TextStyle( fontFamily = Poppins, fontWeight = FontWeight.Normal, - fontSize = 14.sp + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp ), - displayMedium = TextStyle( + labelMedium = TextStyle( fontFamily = Poppins, - fontWeight = FontWeight.Bold, - fontSize = 57.sp + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp ) + /* Anda bisa menambahkan style lain di sini jika diperlukan */ ) diff --git a/app/src/main/res/drawable/ic_female.xml b/app/src/main/res/drawable/ic_female.xml new file mode 100644 index 0000000..7d2ce67 --- /dev/null +++ b/app/src/main/res/drawable/ic_female.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_playstore.png b/app/src/main/res/drawable/ic_launcher_playstore.png new file mode 100644 index 0000000..80d80fe Binary files /dev/null and b/app/src/main/res/drawable/ic_launcher_playstore.png differ diff --git a/app/src/main/res/drawable/ic_male.xml b/app/src/main/res/drawable/ic_male.xml new file mode 100644 index 0000000..64673f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_male.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 264eaf4..036d09b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,22 +1,5 @@ - - - - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 264eaf4..036d09b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,22 +1,5 @@ - - - - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..1f20cb5 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..de6aea6 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..fd2ed83 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..55f64a2 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..7bb89f6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..cc68d8f 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..df9097d 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..1e39197 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..5a35073 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..54dcbf7 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..214c4ad Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..8cbe569 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..11d6fca 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..a35fd7a Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..9062657 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..52b9289 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #1F7A6F + \ No newline at end of file