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.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.*
import androidx.compose.material3.Text import androidx.compose.runtime.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.layout.ContentScale
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.ImeAction
import androidx.compose.ui.text.input.KeyboardType 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.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 @Composable
fun InputWithImage( fun InputWithImage(
@DrawableRes imageRes: Int, @DrawableRes imageRes: Int,
@ -34,8 +255,7 @@ fun InputWithImage(
isError: Boolean, isError: Boolean,
errorMessage: String, errorMessage: String,
imeAction: ImeAction, imeAction: ImeAction,
maxLength: Int, // Parameter baru untuk batas maksimal karakter keyboardActions: KeyboardActions = KeyboardActions.Default, // Tambah parameter ini
keyboardActions: KeyboardActions = KeyboardActions.Default,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Row( Row(
@ -53,12 +273,7 @@ fun InputWithImage(
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
OutlinedTextField( OutlinedTextField(
value = value, value = value,
onValueChange = { onValueChange = onValueChanged,
// LOGIKA BARU UNTUK MEMBATASI INPUT
if (it.length <= maxLength) {
onValueChanged(it)
}
},
label = { Text(stringResource(label)) }, label = { Text(stringResource(label)) },
keyboardOptions = KeyboardOptions( keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number, keyboardType = KeyboardType.Number,
@ -67,7 +282,7 @@ fun InputWithImage(
singleLine = true, singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
isError = isError, isError = isError,
keyboardActions = keyboardActions keyboardActions = keyboardActions // Terapkan keyboardActions
) )
if (isError) { if (isError) {
Text( 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()
}
}
}