diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f786714..38be2fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,4 @@ -/* - * Copyright (C) 2023 The Android Open Source Project +/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +20,13 @@ plugins { } android { - compileSdk = 35 + namespace = "com.example.tiptime" + compileSdk = 34 defaultConfig { applicationId = "com.example.tiptime" minSdk = 24 - targetSdk = 35 + targetSdk = 34 versionCode = 1 versionName = "1.0" @@ -46,40 +46,51 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = "1.8" } buildFeatures { compose = true } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } - namespace = "com.example.tiptime" } dependencies { - implementation(platform("androidx.compose:compose-bom:2024.12.01")) - implementation("androidx.activity:activity-compose:1.9.3") + // Gunakan Compose Bill of Materials (BOM) versi stabil + val composeBom = platform("androidx.compose:compose-bom:2024.06.00") + implementation(composeBom) + androidTestImplementation(composeBom) + + // Dependensi Compose (versi akan diatur oleh BOM) implementation("androidx.compose.material3:material3") implementation("androidx.compose.ui:ui") - implementation("androidx.compose.ui:ui-tooling") + implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-tooling-preview") - implementation("androidx.core:core-ktx:1.15.0") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") + implementation("androidx.compose.foundation:foundation") - testImplementation("junit:junit:4.13.2") - - androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01")) - androidTestImplementation("androidx.compose.ui:ui-test-junit4") - androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") - androidTestImplementation("androidx.test.ext:junit:1.2.1") + // Dependensi AndroidX lainnya + implementation("androidx.core:core-ktx:1.13.1") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3") + implementation("androidx.activity:activity-compose:1.9.0") + // Dependensi untuk debug + debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-test-manifest") + + // Dependensi testing + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") } diff --git a/app/src/main/java/com/example/tiptime/MainActivity.kt b/app/src/main/java/com/example/tiptime/MainActivity.kt index d0fdd80..e5b2273 100644 --- a/app/src/main/java/com/example/tiptime/MainActivity.kt +++ b/app/src/main/java/com/example/tiptime/MainActivity.kt @@ -1,18 +1,3 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package com.example.tiptime import android.os.Bundle @@ -21,41 +6,31 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -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.statusBarsPadding -import androidx.compose.foundation.layout.wrapContentWidth +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.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable +import androidx.compose.material3.* +import androidx.compose.runtime.* 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.graphics.Color import androidx.compose.ui.res.painterResource 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.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.text.NumberFormat class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -65,150 +40,259 @@ class MainActivity : ComponentActivity() { TipTimeTheme { Surface( modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background ) { - TipTimeLayout() + NavigationController() + } + } + } + } +} + +// Enum dan NavigationController tetap sama +private sealed class Screen { + object Start : Screen() + object Main : Screen() +} + +@Composable +private fun NavigationController() { + var currentScreen by remember { mutableStateOf(Screen.Start) } + + when (currentScreen) { + is Screen.Start -> { + StartScreen(onNavigateToMain = { currentScreen = Screen.Main }) + } + is Screen.Main -> { + BMICalculatorScreen() + } + } +} + + +// --- INI ADALAH TAMPILAN BARU YANG LEBIH MENARIK --- +@Composable +fun BMICalculatorScreen() { + var heightInput by remember { mutableStateOf("") } + var weightInput by remember { mutableStateOf("") } + var useMetric by remember { mutableStateOf(true) } + + val height = heightInput.toDoubleOrNull() ?: 0.0 + val weight = weightInput.toDoubleOrNull() ?: 0.0 + + val bmiValue = calculateBMI(height, weight, useMetric) + val bmiCategory = bmiCategory(bmiValue) + 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.headlineMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(vertical = 16.dp) + ) + + // --- KARTU HASIL BMI --- + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + 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( + text = "%.1f".format(bmiValue), + fontSize = 52.sp, + fontWeight = FontWeight.Bold, + 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) + } + } + + // --- KARTU INPUT DATA --- + Card( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + UnitSwitchRow( + isMetric = useMetric, + onUnitChanged = { useMetric = it } + ) + Spacer(modifier = Modifier.height(24.dp)) + + val heightLabel = if (useMetric) R.string.height_cm else R.string.height_in + EditNumberField( + label = heightLabel, + leadingIcon = R.drawable.ic_height, + value = heightInput, + onValueChanged = { heightInput = it }, + imeAction = ImeAction.Next + ) + + Spacer(modifier = Modifier.height(16.dp)) + + val weightLabel = if (useMetric) R.string.weight_kg else R.string.weight_lbs + EditNumberField( + label = weightLabel, + leadingIcon = R.drawable.ic_weight, + value = weightInput, + onValueChanged = { weightInput = it }, + imeAction = ImeAction.Done + ) + } + } + } +} + +@Composable +fun BmiIndicatorBar(category: String) { + val categories = listOf("Underweight", "Normal", "Overweight", "Obese") + Row( + modifier = Modifier + .fillMaxWidth() + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface) + .padding(2.dp) + ) { + categories.forEach { cat -> + Box( + modifier = Modifier + .weight(1f) + .height(12.dp) + .background( + if (cat == category) getBmiCategoryColor(cat) else Color.LightGray.copy(alpha = 0.5f) + ) + ) { + if (cat == category) { + Text( + text = cat, + color = Color.White, + fontSize = 8.sp, // Ukuran font sangat kecil + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.Center) + ) } } } } } -@Composable -fun TipTimeLayout() { - var amountInput by remember { mutableStateOf("") } - var tipInput by remember { mutableStateOf("") } - var roundUp by remember { mutableStateOf(false) } - - val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0 - val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0 - val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp) - val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp) - - Column( - modifier = Modifier - .statusBarsPadding() - .padding(horizontal = 40.dp) - .verticalScroll(rememberScrollState()) - .safeDrawingPadding(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = stringResource(R.string.calculate_tip), - modifier = Modifier - .padding(bottom = 16.dp, top = 40.dp) - .align(alignment = Alignment.Start) - ) - EditNumberField( - label = R.string.height, - leadingIcon = R.drawable.number, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Next - ), - value = amountInput, - onValueChanged = { amountInput = it }, - modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), - ) - EditNumberField( - label = R.string.weight, - leadingIcon = R.drawable.number, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - value = tipInput, - onValueChanged = { tipInput = it }, - modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(), - ) - RoundTheTipRow( - roundUp = roundUp, - onRoundUpChanged = { roundUp = it }, - modifier = Modifier.padding(bottom = 32.dp) - ) - Text( - text = stringResource(R.string.bmi_calculation, bmi), - style = MaterialTheme.typography.displaySmall - ) - Text( - text = stringResource(R.string.bmi_category, category), - style = MaterialTheme.typography.displaySmall - ) - - Spacer(modifier = Modifier.height(150.dp)) - } -} - @Composable fun EditNumberField( @StringRes label: Int, @DrawableRes leadingIcon: Int, - keyboardOptions: KeyboardOptions, value: String, onValueChanged: (String) -> Unit, - modifier: Modifier = Modifier + imeAction: ImeAction ) { - TextField( + OutlinedTextField( value = value, - singleLine = true, - leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) }, - modifier = modifier, onValueChange = onValueChanged, label = { Text(stringResource(label)) }, - keyboardOptions = keyboardOptions + leadingIcon = { Icon(painter = painterResource(id = leadingIcon), contentDescription = null) }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = imeAction + ), + singleLine = true, + modifier = Modifier.fillMaxWidth() ) } @Composable -fun RoundTheTipRow( - roundUp: Boolean, - onRoundUpChanged: (Boolean) -> Unit, - modifier: Modifier = Modifier -) { +fun UnitSwitchRow(isMetric: Boolean, onUnitChanged: (Boolean) -> Unit) { Row( - modifier = modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - Text(text = stringResource(R.string.use_usc)) - Switch( - modifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.End), - checked = roundUp, - onCheckedChange = onRoundUpChanged - ) + Text(text = stringResource(R.string.unit_system)) + 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) + } } } -/** - * Calculates the BMI - * - * Catatan: tambahkan unit test untuk kalkulasi BMI ini - */ -private fun calculateBMI(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String { - var bmi = BmiWeight / 100 * BmiHeight - if (roundUp) { - bmi = kotlin.math.ceil(bmi) - } - return NumberFormat.getNumberInstance().format(bmi) -} -/** - * Calculates the BMI Category - * - * Catatan: tambahkan unit test untuk kalkulasi BMI ini - */ +// --- FUNGSI LOGIKA (HELPER) --- -private fun calculateBMICategory(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String { - var bmi = BmiWeight / 100 * BmiHeight - if (roundUp) { - bmi = kotlin.math.ceil(bmi) +private fun calculateBMI(height: Double, weight: Double, isMetric: Boolean): Double { + if (height <= 0 || weight <= 0) return 0.0 + + return if (isMetric) { + val heightInMeters = height / 100 + weight / (heightInMeters * heightInMeters) + } else { + 703 * weight / (height * height) } - return NumberFormat.getNumberInstance().format(bmi) } -@Preview(showBackground = true) + +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 -fun TipTimeLayoutPreview() { - TipTimeTheme { - TipTimeLayout() +private fun getBmiCategoryColor(category: String): Color { + return when (category) { + "Underweight" -> Color(0xFF8AB4F8) // Biru + "Normal" -> Color(0xFF5BB974) // Hijau + "Overweight" -> Color(0xFFFDD663) // Kuning + "Obese" -> Color(0xFFE57373) // Merah + else -> MaterialTheme.colorScheme.onSurface } -} \ No newline at end of file +} + +// --- PREVIEW --- + +@Preview(showBackground = true, name = "New BMI Calculator") +@Composable +fun BMICalculatorScreenPreview() { + TipTimeTheme { + BMICalculatorScreen() + } +} diff --git a/app/src/main/java/com/example/tiptime/StartScreen.kt b/app/src/main/java/com/example/tiptime/StartScreen.kt new file mode 100644 index 0000000..cd0edce --- /dev/null +++ b/app/src/main/java/com/example/tiptime/StartScreen.kt @@ -0,0 +1,109 @@ +package com.example.tiptime + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +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 utama untuk Start Screen yang interaktif +@Composable +fun StartScreen(onNavigateToMain: () -> Unit) { + // State untuk memulai dan mengontrol animasi + var startAnimation by remember { mutableStateOf(false) } + val alphaAnim = animateFloatAsState( + targetValue = if (startAnimation) 1f else 0f, + animationSpec = tween(durationMillis = 2000), // Animasi logo & teks muncul selama 2 detik + label = "alpha_animation" + ) + + // LaunchedEffect sekarang hanya untuk memulai animasi saat layar pertama kali muncul + LaunchedEffect(key1 = true) { + startAnimation = true // Mulai animasi + } + + // Tampilan UI untuk Start Screen + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // --- Bagian Logo dan Judul (tetap sama) --- + Image( + painter = painterResource(id = R.drawable.ic_weight), // Menggunakan ikon timbangan + contentDescription = "App Logo", + modifier = Modifier + .size(120.dp) + .alpha(alphaAnim.value) // Terapkan animasi alpha + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(id = R.string.app_name), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.alpha(alphaAnim.value) // Terapkan animasi alpha + ) + + Spacer(modifier = Modifier.height(100.dp)) // Beri jarak lebih ke tombol + + // --- Bagian Tombol Interaktif --- + // AnimatedVisibility akan membuat tombol muncul dengan animasi setelah logo tampil + AnimatedVisibility( + visible = startAnimation, + enter = slideInVertically( + initialOffsetY = { it }, // Muncul dari bawah + animationSpec = tween(durationMillis = 1000, delayMillis = 500) // Animasi setelah 0.5 detik + ) + ) { + Button( + // Saat tombol diklik, panggil fungsi untuk navigasi + onClick = onNavigateToMain, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp) + ) { + Text( + text = "Mulai Hitung", + fontSize = 18.sp + ) + } + } + } +} + +// Preview khusus untuk StartScreen agar bisa dilihat di panel desain +@Preview(showBackground = true, name = "Start Screen Preview") +@Composable +fun StartScreenPreview() { + TipTimeTheme { + StartScreen(onNavigateToMain = {}) + } +} diff --git a/app/src/main/res/drawable/ic_height.png b/app/src/main/res/drawable/ic_height.png new file mode 100644 index 0000000..d38845b Binary files /dev/null and b/app/src/main/res/drawable/ic_height.png differ diff --git a/app/src/main/res/drawable/ic_weight.png b/app/src/main/res/drawable/ic_weight.png new file mode 100644 index 0000000..85a030e Binary files /dev/null and b/app/src/main/res/drawable/ic_weight.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04e6db2..ffa5269 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,10 +16,18 @@ --> BMI Calculator - Calculate BMI - Tinggi Badan - Berat Badan - Gunakan Unit USC (lbs/in)? - BMI Anda: %s - Kategori: %s + BMI Calculator + Height (cm) + Height (in) + Weight (kg) + Weight (lbs) + Metric (kg, cm) + Imperial (lbs, in) + Your BMI: %s + Category: %s + Your BMI is + Unit System + + +