final submit

This commit is contained in:
HagaDalpintoGinting 2025-11-07 21:34:11 +07:00
parent cdaf581693
commit e74b4b1d67

View File

@ -1,30 +1,251 @@
package com.example.tiptime
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
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.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.error
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
// --- Composable InputWithImage DIPERBARUI dengan Batas Karakter ---
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
TipTimeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surfaceContainerLowest
) {
NavigationController()
}
}
}
}
}
// Navigasi tidak berubah
private sealed class Screen {
object Start : Screen()
object Main : Screen()
}
@Composable
private fun NavigationController() {
var currentScreen by remember { mutableStateOf<Screen>(Screen.Start) }
when (currentScreen) {
is Screen.Start -> {
StartScreen(onNavigateToMain = { currentScreen = Screen.Main })
}
is Screen.Main -> {
BMICalculatorScreen()
}
}
}
// --- VERSI DENGAN TOMBOL HITUNG ---
@Composable
fun BMICalculatorScreen() {
// --- State untuk input pengguna ---
var heightInput by remember { mutableStateOf("") }
var weightInput by remember { mutableStateOf("") }
var useMetric by remember { mutableStateOf(true) }
// --- State untuk hasil perhitungan (dipisah) ---
var calculatedBmi by remember { mutableStateOf(0.0) }
// State untuk status error
var isHeightError by remember { mutableStateOf(false) }
var isWeightError by remember { mutableStateOf(false) }
// Batas Maksimal
val maxHeightCm = 250.0
val maxWeightKg = 300.0
// Keyboard controller untuk menyembunyikan keyboard setelah tombol ditekan
val keyboardController = LocalSoftwareKeyboardController.current
// --- FUNGSI BARU YANG DIPANGGIL SAAT TOMBOL DITEKAN ---
fun performCalculation() {
keyboardController?.hide() // Sembunyikan keyboard
val height = heightInput.toDoubleOrNull() ?: 0.0
val weight = weightInput.toDoubleOrNull() ?: 0.0
// Lakukan validasi saat tombol ditekan
isHeightError = height <= 0 || height > maxHeightCm
isWeightError = weight <= 0 || weight > maxWeightKg
// Hanya hitung dan perbarui state hasil jika tidak ada error
if (!isHeightError && !isWeightError) {
calculatedBmi = calculateBMI(height, weight, useMetric)
} else {
calculatedBmi = 0.0 // Reset hasil jika ada error
}
}
// Menggunakan state hasil (calculatedBmi) untuk tampilan kartu atas
val bmiCategory = bmiCategory(calculatedBmi)
val bmiCategoryColor = getBmiCategoryColor(bmiCategory)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.safeDrawingPadding()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.bmi_calculator),
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 16.dp)
)
// --- Kartu Hasil BMI ---
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(id = R.string.your_bmi_is),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
// Menampilkan dari state hasil 'calculatedBmi'
text = "%.1f".format(calculatedBmi),
fontSize = 52.sp,
fontWeight = FontWeight.ExtraBold,
color = bmiCategoryColor
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = bmiCategory,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = bmiCategoryColor
)
Spacer(modifier = Modifier.height(16.dp))
BmiIndicatorBar(category = bmiCategory)
}
}
// --- Bagian Input Data ---
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp) // Jarak antar kartu
) {
Text(
text = "Pengaturan & Input",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp),
fontWeight = FontWeight.Bold
)
Card(modifier = Modifier.fillMaxWidth()) {
UnitSwitchRow(
isMetric = useMetric,
onUnitChanged = { useMetric = it },
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
Card(modifier = Modifier.fillMaxWidth()) {
InputWithImage(
imageRes = R.drawable.ic_height,
label = if (useMetric) R.string.height_cm else R.string.height_in,
value = heightInput,
onValueChanged = {
heightInput = it
// Reset error saat pengguna mulai mengetik lagi
isHeightError = false
},
isError = isHeightError,
errorMessage = "Input tidak valid (1-${maxHeightCm.toInt()})",
imeAction = ImeAction.Next,
modifier = Modifier.padding(16.dp)
)
}
Card(modifier = Modifier.fillMaxWidth()) {
InputWithImage(
imageRes = R.drawable.ic_weight,
label = if (useMetric) R.string.weight_kg else R.string.weight_lbs,
value = weightInput,
onValueChanged = {
weightInput = it
// Reset error saat pengguna mulai mengetik lagi
isWeightError = false
},
isError = isWeightError,
errorMessage = "Input tidak valid (1-${maxWeightKg.toInt()})",
imeAction = ImeAction.Done,
// Tambahkan keyboardActions untuk memicu perhitungan saat "Done" ditekan
keyboardActions = KeyboardActions(onDone = { performCalculation() }),
modifier = Modifier.padding(16.dp)
)
}
}
Spacer(modifier = Modifier.weight(1f)) // Spacer untuk mendorong tombol ke bawah
// --- TOMBOL HITUNG BARU ---
Button(
onClick = { performCalculation() }, // Panggil fungsi perhitungan
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
.height(50.dp)
) {
Text("HITUNG BMI", fontSize = 18.sp, fontWeight = FontWeight.Bold)
}
}
}
// --- Composable InputWithImage diperbarui ---
@Composable
fun InputWithImage(
@DrawableRes imageRes: Int,
@ -34,8 +255,7 @@ fun InputWithImage(
isError: Boolean,
errorMessage: String,
imeAction: ImeAction,
maxLength: Int, // Parameter baru untuk batas maksimal karakter
keyboardActions: KeyboardActions = KeyboardActions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default, // Tambah parameter ini
modifier: Modifier = Modifier
) {
Row(
@ -53,12 +273,7 @@ fun InputWithImage(
Column(modifier = Modifier.weight(1f)) {
OutlinedTextField(
value = value,
onValueChange = {
// LOGIKA BARU UNTUK MEMBATASI INPUT
if (it.length <= maxLength) {
onValueChanged(it)
}
},
onValueChange = onValueChanged,
label = { Text(stringResource(label)) },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
@ -67,7 +282,7 @@ fun InputWithImage(
singleLine = true,
modifier = Modifier.fillMaxWidth(),
isError = isError,
keyboardActions = keyboardActions
keyboardActions = keyboardActions // Terapkan keyboardActions
)
if (isError) {
Text(
@ -80,3 +295,96 @@ fun InputWithImage(
}
}
}
// --- FUNGSI-FUNGSI PEMBANTU LAINNYA ---
// (Tidak ada perubahan signifikan di sini)
@Composable
fun BmiIndicatorBar(category: String) {
val categories = listOf("Underweight", "Normal", "Overweight", "Obese")
Row(
modifier = Modifier
.fillMaxWidth()
.height(16.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.surface)
.padding(2.dp)
) {
categories.forEach { cat ->
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.background(
if (cat == category) getBmiCategoryColor(cat) else Color.LightGray.copy(
alpha = 0.3f
)
)
)
}
}
}
@Composable
fun UnitSwitchRow(isMetric: Boolean, onUnitChanged: (Boolean) -> Unit, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Unit", fontWeight = FontWeight.SemiBold)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Imperial", style = MaterialTheme.typography.bodyMedium)
Switch(
checked = isMetric,
onCheckedChange = onUnitChanged,
modifier = Modifier.padding(horizontal = 8.dp)
)
Text("Metric", style = MaterialTheme.typography.bodyMedium)
}
}
}
private fun calculateBMI(height: Double, weight: Double, isMetric: Boolean): Double {
if (height <= 0 || weight <= 0) return 0.0
if (height > 250 || weight > 300) return 0.0
return if (isMetric) {
val heightInMeters = height / 100
weight / (heightInMeters * heightInMeters)
} else {
703 * weight / (height * height)
}
}
private fun bmiCategory(bmi: Double): String {
return when {
bmi == 0.0 -> "..."
bmi < 18.5 -> "Underweight"
bmi < 25.0 -> "Normal"
bmi < 30.0 -> "Overweight"
else -> "Obese"
}
}
@Composable
private fun getBmiCategoryColor(category: String): Color {
return when (category) {
"Underweight" -> Color(0xFF8AB4F8)
"Normal" -> Color(0xFF5BB974)
"Overweight" -> Color(0xFFFDD663)
"Obese" -> Color(0xFFE57373)
else -> MaterialTheme.colorScheme.onSurface
}
}
@Preview(showBackground = true, name = "Final BMI Calculator with Button")
@Composable
fun BMICalculatorScreenPreview() {
TipTimeTheme {
Surface(color = MaterialTheme.colorScheme.surfaceContainerLowest) {
BMICalculatorScreen()
}
}
}