Melakukan Update:

- Perubahan Tema
- Penambahan Speedometer BMI
- Gender
- Umur
This commit is contained in:
202310715312 HADI GUNA PRAKOSO 2025-11-07 20:31:44 +07:00
parent 099c35f19a
commit 9a330b4085
10 changed files with 549 additions and 381 deletions

View File

@ -1,19 +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.
*/
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
@ -21,12 +5,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,11 +31,11 @@ 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
@ -60,26 +45,23 @@ android {
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")
implementation("androidx.compose.material3:material3")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.8.2")
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
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.material3:material3")
implementation("androidx.compose.material:material-icons-extended")
testImplementation("junit:junit:4.13.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}

View File

@ -1,214 +1,373 @@
/*
* 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.
*/
// NAMA: HADI PRAKOSO
// NPM: 2110010323
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.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.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
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.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.SwapVert
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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
import java.util.Locale
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
TipTimeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
) {
TipTimeLayout()
TipTimeTheme(darkTheme = true) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
BmiCalculatorScreen()
}
}
}
}
}
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
var roundUp by remember { mutableStateOf(false) }
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
)
val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0
val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0
val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp)
val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp)
@Composable
fun BmiCalculatorScreen() {
var heightInput by remember { mutableStateOf("") }
var weightInput by remember { mutableStateOf("") }
var ageInput by remember { mutableStateOf("") }
var selectedGender by remember { mutableStateOf("Laki-laki") }
var useMetricUnits by remember { mutableStateOf(true) }
var bmiResult by remember { mutableStateOf<BmiFullResult?>(null) }
var validationError by remember { mutableStateOf<String?>(null) }
fun resetFields() {
heightInput = ""
weightInput = ""
ageInput = ""
selectedGender = "Laki-laki"
useMetricUnits = true
bmiResult = null
validationError = null
}
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
.fillMaxSize()
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
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
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))
AnimatedVisibility(visible = bmiResult != null) {
bmiResult?.let { ResultDisplay(it) }
}
if (bmiResult != null) {
Spacer(Modifier.height(24.dp))
}
InputPanel(
heightInput = heightInput, onHeightChange = { heightInput = it; validationError = null },
weightInput = weightInput, onWeightChange = { weightInput = it; validationError = null },
ageInput = ageInput, onAgeChange = { ageInput = it; validationError = null },
selectedGender = selectedGender, onGenderSelect = { selectedGender = it },
useMetric = useMetricUnits, onUnitChange = { useMetricUnits = it }
)
Spacer(modifier = Modifier.height(150.dp))
validationError?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp)
)
}
Spacer(Modifier.height(24.dp))
ActionButtons(
onCalculate = {
val height = heightInput.toDoubleOrNull()
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
}
else -> {
validationError = null
bmiResult = calculateBmi(weight, height, useMetricUnits)
}
}
},
onReset = { resetFields() }
)
}
}
@Composable
fun EditNumberField(
@StringRes label: Int,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
fun ResultDisplay(result: BmiFullResult) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
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)
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))
ResultDetails(result)
}
}
}
@Composable
fun BmiGauge(bmi: Double) {
val minBmi = 15.0
val maxBmi = 40.0
val clampedBmi = bmi.coerceIn(minBmi, maxBmi)
val progress = if (bmi > 0) ((clampedBmi - minBmi) / (maxBmi - minBmi)).toFloat() else 0f
val angle = 180f + progress * 180f
val animatedAngle by animateFloatAsState(targetValue = angle, label = "BMI Angle")
val density = LocalDensity.current
val gaugeWidth = with(density) { 25.dp.toPx() }
val underweightColor = Color(0xFFD32F2F)
val normalColor = MaterialTheme.colorScheme.primary
val overweightColor = Color(0xFFFFEB3B)
val obeseColor = Color(0xFFB71C1C)
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
val surfaceColor = MaterialTheme.colorScheme.surface
Box(modifier = Modifier.size(280.dp).padding(20.dp), contentAlignment = Alignment.Center) {
Canvas(modifier = Modifier.fillMaxSize()) {
val center = Offset(size.width / 2, size.height / 2)
val radius = (size.minDimension / 2) - gaugeWidth / 2
val bmiRange = maxBmi - minBmi
val underweightSweep = ((18.5 - minBmi) / bmiRange * 180f).toFloat()
val normalSweep = ((25.0 - 18.5) / bmiRange * 180f).toFloat()
val overweightSweep = ((30.0 - 25.0) / bmiRange * 180f).toFloat()
val obeseSweep = ((maxBmi - 30.0) / bmiRange * 180f).toFloat()
drawArc(underweightColor, 180f, underweightSweep, false, style = Stroke(gaugeWidth, cap = StrokeCap.Butt))
drawArc(normalColor, 180f + underweightSweep, normalSweep, false, style = Stroke(gaugeWidth, cap = StrokeCap.Butt))
drawArc(overweightColor, 180f + underweightSweep + normalSweep, overweightSweep, false, style = Stroke(gaugeWidth, cap = StrokeCap.Butt))
drawArc(obeseColor, 180f + underweightSweep + normalSweep + overweightSweep, obeseSweep, false, style = Stroke(gaugeWidth, cap = StrokeCap.Butt))
val angleRad = Math.toRadians(animatedAngle.toDouble()).toFloat()
val needleLength = radius
val endOffset = Offset(center.x + needleLength * cos(angleRad), center.y + needleLength * sin(angleRad))
drawLine(onSurfaceColor, center, endOffset, 5.dp.toPx(), StrokeCap.Round)
drawCircle(onSurfaceColor, radius = 10.dp.toPx(), center = center)
drawCircle(surfaceColor, radius = 5.dp.toPx(), center = center)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "BMI", style = MaterialTheme.typography.bodyLarge)
Text(
text = if (bmi > 0) String.format(Locale.getDefault(), "%.1f", bmi) else "-",
style = MaterialTheme.typography.displayMedium.copy(fontWeight = FontWeight.Bold, color = onSurfaceColor)
)
}
}
}
@Composable
fun ResultDetails(result: BmiFullResult) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
Text("Rentang BMI sehat: ${result.healthyBmiRange} kg/m²", style = MaterialTheme.typography.bodyLarge)
Text("Berat badan sehat untuk tinggi Anda: ${result.healthyWeightRange}", style = MaterialTheme.typography.bodyLarge)
Text("BMI Prime: ${String.format("%.2f", result.bmiPrime)}", style = MaterialTheme.typography.bodyLarge)
Text("Indeks Ponderal: ${String.format("%.1f", result.ponderalIndex)} kg/m³", style = MaterialTheme.typography.bodyLarge)
}
}
@Composable
fun InputPanel(
heightInput: String, onHeightChange: (String) -> Unit,
weightInput: String, onWeightChange: (String) -> Unit,
ageInput: String, onAgeChange: (String) -> Unit,
selectedGender: String, onGenderSelect: (String) -> Unit,
useMetric: Boolean, onUnitChange: (Boolean) -> Unit
) {
TextField(
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)
}
}
@Composable
fun ActionButtons(onCalculate: () -> Unit, onReset: () -> Unit) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Button(onClick = onCalculate, modifier = Modifier.weight(1f).height(50.dp), shape = RoundedCornerShape(16.dp)) {
Text("Hitung BMI", fontSize = 16.sp, fontWeight = FontWeight.Bold)
}
OutlinedButton(onClick = onReset, modifier = Modifier.weight(1f).height(50.dp), shape = RoundedCornerShape(16.dp)) {
Text("Ulang", fontSize = 16.sp)
}
}
}
@Composable
fun UnitToggle(useMetric: Boolean, onUnitChange: (Boolean) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
Text("Metric", color = if (useMetric) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
Switch(checked = !useMetric, onCheckedChange = { onUnitChange(!it) }, modifier = Modifier.padding(horizontal = 8.dp))
Text("USC", color = if (!useMetric) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant)
}
}
@Composable
fun GenderSelector(selectedOption: String, onOptionSelected: (String) -> Unit) {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
val options = listOf("Laki-laki", "Perempuan")
options.forEach { gender ->
val isSelected = selectedOption == gender
Button(
onClick = { onOptionSelected(gender) },
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant,
contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.Person, contentDescription = gender, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text(gender)
}
}
}
}
@Composable
fun BmiTextField(label: String, value: String, onValueChange: (String) -> Unit, icon: androidx.compose.ui.graphics.vector.ImageVector) {
OutlinedTextField(
value = value,
singleLine = true,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
modifier = modifier,
onValueChange = onValueChanged,
label = { Text(stringResource(label)) },
keyboardOptions = keyboardOptions
onValueChange = onValueChange,
label = { Text(label) },
leadingIcon = { Icon(icon, contentDescription = label) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
}
@Composable
fun RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.use_usc))
Switch(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
checked = roundUp,
onCheckedChange = onRoundUpChanged
)
// --- 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"
}
}
/**
* 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
*/
private fun calculateBMICategory(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)
}
@Preview(showBackground = true)
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
fun BmiCalculatorScreenPreview() {
TipTimeTheme(darkTheme = true) {
BmiCalculatorScreen()
}
}

View File

@ -1,78 +1,67 @@
/*
* 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.ui.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF984061)
// COMPLETE Material 3 Color Palette (Purple Theme)
// Light Theme Colors
val md_theme_light_primary = Color(0xFF6750A4)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFD9E2)
val md_theme_light_onPrimaryContainer = Color(0xFF3E001D)
val md_theme_light_secondary = Color(0xFF754B9C)
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
val md_theme_light_secondary = Color(0xFF625B71)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFF1DBFF)
val md_theme_light_onSecondaryContainer = Color(0xFF2D0050)
val md_theme_light_tertiary = Color(0xFF984060)
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
val md_theme_light_tertiary = Color(0xFF7D5260)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFFFD9E2)
val md_theme_light_onTertiaryContainer = Color(0xFF3E001D)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4)
val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
val md_theme_light_error = Color(0xFFB3261E)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFAFCFF)
val md_theme_light_onBackground = Color(0xFF001F2A)
val md_theme_light_surface = Color(0xFFFAFCFF)
val md_theme_light_onSurface = Color(0xFF001F2A)
val md_theme_light_surfaceVariant = Color(0xFFF2DDE2)
val md_theme_light_onSurfaceVariant = Color(0xFF514347)
val md_theme_light_outline = Color(0xFF837377)
val md_theme_light_inverseOnSurface = Color(0xFFE1F4FF)
val md_theme_light_inverseSurface = Color(0xFF003547)
val md_theme_light_inversePrimary = Color(0xFFFFB0C8)
val md_theme_light_surfaceTint = Color(0xFF984061)
val md_theme_light_outlineVariant = Color(0xFFD5C2C6)
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
val md_theme_light_background = Color(0xFFFFFBFE)
val md_theme_light_onBackground = Color(0xFF1C1B1F)
val md_theme_light_surface = Color(0xFFFFFBFE)
val md_theme_light_onSurface = Color(0xFF1C1B1F)
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
val md_theme_light_outline = Color(0xFF79747E)
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
val md_theme_light_inverseSurface = Color(0xFF313033)
val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
val md_theme_light_surfaceTint = Color(0xFF6750A4)
val md_theme_light_outlineVariant = Color(0xFFCAC4D0)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFFFFB0C8)
val md_theme_dark_onPrimary = Color(0xFF5E1133)
val md_theme_dark_primaryContainer = Color(0xFF7B2949)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9E2)
val md_theme_dark_secondary = Color(0xFFDEB7FF)
val md_theme_dark_onSecondary = Color(0xFF44196A)
val md_theme_dark_secondaryContainer = Color(0xFF5C3382)
val md_theme_dark_onSecondaryContainer = Color(0xFFF1DBFF)
val md_theme_dark_tertiary = Color(0xFFFFB1C7)
val md_theme_dark_onTertiary = Color(0xFF5E1132)
val md_theme_dark_tertiaryContainer = Color(0xFF7B2948)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E2)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF001F2A)
val md_theme_dark_onBackground = Color(0xFFBFE9FF)
val md_theme_dark_surface = Color(0xFF001F2A)
val md_theme_dark_onSurface = Color(0xFFBFE9FF)
val md_theme_dark_surfaceVariant = Color(0xFF514347)
val md_theme_dark_onSurfaceVariant = Color(0xFFD5C2C6)
val md_theme_dark_outline = Color(0xFF9E8C90)
val md_theme_dark_inverseOnSurface = Color(0xFF001F2A)
val md_theme_dark_inverseSurface = Color(0xFFBFE9FF)
val md_theme_dark_inversePrimary = Color(0xFF984061)
val md_theme_dark_surfaceTint = Color(0xFFFFB0C8)
val md_theme_dark_outlineVariant = Color(0xFF514347)
// Dark Theme Colors
val md_theme_dark_primary = Color(0xFFD0BCFF)
val md_theme_dark_onPrimary = Color(0xFF381E72)
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
val md_theme_dark_secondary = Color(0xFFCCC2DC)
val md_theme_dark_onSecondary = Color(0xFF332D41)
val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
val md_theme_dark_tertiary = Color(0xFFEFB8C8)
val md_theme_dark_onTertiary = Color(0xFF492532)
val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
val md_theme_dark_error = Color(0xFFF2B8B5)
val md_theme_dark_onError = Color(0xFF601410)
val md_theme_dark_errorContainer = Color(0xFF8C1D18)
val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC)
val md_theme_dark_background = Color(0xFF1C1B1F)
val md_theme_dark_onBackground = Color(0xFFE6E1E5)
val md_theme_dark_surface = Color(0xFF1C1B1F)
val md_theme_dark_onSurface = Color(0xFFE6E1E5)
val md_theme_dark_surfaceVariant = Color(0xFF49454F)
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
val md_theme_dark_outline = Color(0xFF938F99)
val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F)
val md_theme_dark_inverseSurface = Color(0xFFE6E1E5)
val md_theme_dark_inversePrimary = Color(0xFF6750A4)
val md_theme_dark_surfaceTint = Color(0xFFD0BCFF)
val md_theme_dark_outlineVariant = Color(0xFF49454F)
val md_theme_dark_scrim = Color(0xFF000000)

View File

@ -0,0 +1,11 @@
package com.example.tiptime.ui.theme
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import com.example.tiptime.R
val Poppins = FontFamily(
Font(R.font.poppins_regular, FontWeight.Normal),
Font(R.font.poppins_bold, FontWeight.Bold)
)

View File

@ -1,29 +1,41 @@
/*
* 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.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.graphics.Color
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFF00C853), // A vibrant green for primary actions
onPrimary = Color.Black,
primaryContainer = Color(0xFF009624),
onPrimaryContainer = Color.White,
secondary = Color(0xFF00BFA5), // A teal for secondary elements
onSecondary = Color.Black,
secondaryContainer = Color(0xFF008E76),
onSecondaryContainer = Color.White,
tertiary = Color(0xFF00B8D4), // A cyan for accents
onTertiary = Color.Black,
tertiaryContainer = Color(0xFF008394),
onTertiaryContainer = Color.White,
background = Color(0xFF121212), // A very dark background
onBackground = Color.White,
surface = Color(0xFF1E1E1E), // A slightly lighter surface for cards
onSurface = Color.White,
surfaceVariant = Color(0xFF424242),
onSurfaceVariant = Color.White,
outline = Color(0xFF9E9E9E),
inverseOnSurface = Color.White,
inverseSurface = Color(0xFF212121),
inversePrimary = Color(0xFF00C853),
error = Color(0xFFD32F2F),
onError = Color.White,
errorContainer = Color(0xFFB71C1C),
onErrorContainer = Color.White
)
private val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary,
@ -39,8 +51,8 @@ private val LightColorScheme = lightColorScheme(
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
errorContainer = md_theme_light_errorContainer,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
@ -50,62 +62,16 @@ private val LightColorScheme = lightColorScheme(
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim,
)
private val DarkColorScheme = darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
)
@Composable
fun TipTimeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
// Dynamic color in this app is turned off for learning purposes
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
MaterialTheme(
colorScheme = colorScheme,

View File

@ -1,33 +1,36 @@
/*
* 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.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
displaySmall = TextStyle(
fontFamily = FontFamily.Default,
// Override default text styles to use Poppins
headlineMedium = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Bold,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp,
fontSize = 28.sp
),
titleMedium = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
bodyLarge = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
bodyMedium = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
),
displayMedium = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Bold,
fontSize = 57.sp
)
)

Binary file not shown.

Binary file not shown.

View File

@ -1,25 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources>
<string name="app_name">BMI Calculator</string>
<string name="calculate_tip">Calculate BMI</string>
<string name="height">Tinggi Badan</string>
<string name="weight">Berat Badan</string>
<string name="use_usc">Gunakan Unit USC (lbs/in)?</string>
<string name="bmi_calculation">BMI Anda: %s</string>
<string name="bmi_category">Kategori: %s</string>
<string name="bmi_calculator_title">BMI Calculator</string>
<string name="age">Age</string>
<string name="height_m">Height (m/cm)</string>
<string name="height_in">Height (in)</string>
<string name="weight_kg">Weight (kg)</string>
<string name="weight_lbs">Weight (lbs)</string>
<string name="si_units">SI Units</string>
<string name="usc_units">USC Units</string>
<string name="your_bmi">Your BMI</string>
<string name="awaiting_input">Awaiting input</string>
</resources>

View File

@ -0,0 +1,69 @@
// NAMA: HADI PRAKOSO
// NPM: 202310715312
package com.example.tiptime
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
class BmiCalculatorTest {
@Test
fun calculateBmi_metric_normal() {
val weight = 70.0 // kg
val height = 175.0 // cm
val result = calculateBmi(weight, height, useMetric = true)
// Pastikan hasil tidak null sebelum melakukan assert pada nilainya
assertNotNull(result)
if (result != null) {
// BMI untuk 70kg dan 175cm adalah ~22.9
assertEquals(22.9, result.bmi, 0.1)
assertEquals("Normal", result.category)
}
}
@Test
fun calculateBmi_usc_overweight() {
val weight = 180.0 // lbs
val height = 68.0 // inches
val result = calculateBmi(weight, height, useMetric = false)
assertNotNull(result)
if (result != null) {
assertEquals(27.4, result.bmi, 0.1)
assertEquals("Berat badan lebih", result.category)
}
}
@Test
fun determineBmiCategory_underweight() {
val category = determineBmiCategory(18.4)
assertEquals("Berat badan kurang", category)
}
@Test
fun determineBmiCategory_normal() {
val category = determineBmiCategory(22.0)
assertEquals("Normal", category)
}
@Test
fun determineBmiCategory_overweight() {
val category = determineBmiCategory(27.0)
assertEquals("Berat badan lebih", category)
}
@Test
fun determineBmiCategory_obesity() {
val category = determineBmiCategory(31.0)
assertEquals("Obesitas", category)
}
@Test
fun calculateBmi_zeroHeight_returnsNull() {
val result = calculateBmi(70.0, 0.0, useMetric = true)
assertEquals(null, result)
}
}