UTS-202310715082-FazriAbdurrahman

This commit is contained in:
202310715082 FAZRI ABDURRAHMAN 2025-11-07 20:47:13 +07:00
parent 099c35f19a
commit de59cc8f47
3 changed files with 257 additions and 190 deletions

View File

@ -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,7 +6,9 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -30,17 +17,18 @@ 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.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.wrapContentWidth
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.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
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
@ -48,14 +36,20 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.tiptime.ui.theme.TipTimeTheme
import java.text.NumberFormat
import kotlin.math.cos
import kotlin.math.sin
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -65,8 +59,9 @@ class MainActivity : ComponentActivity() {
TipTimeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
TipTimeLayout()
BmiCalculatorScreen()
}
}
}
@ -74,68 +69,60 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
var roundUp by remember { mutableStateOf(false) }
fun BmiCalculatorScreen() {
var heightInput by remember { mutableStateOf("175") } // Default for preview
var weightInput by remember { mutableStateOf("70") } // Default for preview
var ageInput by remember { mutableStateOf("25") }
val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0
val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0
val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp)
val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp)
val heightCm = heightInput.toDoubleOrNull() ?: 0.0
val weightKg = weightInput.toDoubleOrNull() ?: 0.0
val bmi = calculateBMI(heightCm, weightKg)
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
verticalArrangement = Arrangement.Top
) {
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(24.dp))
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(),
value = heightInput,
onValueChanged = { heightInput = it },
)
Spacer(modifier = Modifier.height(16.dp))
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(),
value = weightInput,
onValueChanged = { weightInput = it },
)
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(16.dp))
EditNumberField(
label = R.string.age,
leadingIcon = R.drawable.number,
value = ageInput,
onValueChanged = { ageInput = it },
imeAction = ImeAction.Done
)
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(150.dp))
if (bmi > 0) {
ResultScreen(bmi = bmi, heightCm = heightCm, weightKg = weightKg)
}
Spacer(modifier = Modifier.height(20.dp))
}
}
@ -143,72 +130,162 @@ fun TipTimeLayout() {
fun EditNumberField(
@StringRes label: Int,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
imeAction: ImeAction = ImeAction.Next
) {
TextField(
OutlinedTextField(
value = value,
singleLine = true,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
modifier = modifier,
modifier = Modifier.fillMaxWidth(),
onValueChange = onValueChanged,
label = { Text(stringResource(label)) },
keyboardOptions = keyboardOptions
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = imeAction
),
shape = RoundedCornerShape(16.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
)
}
@Composable
fun RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
fun ResultScreen(bmi: Double, heightCm: Double, weightKg: Double) {
val category = getBmiCategory(bmi)
val (healthyWeightMin, healthyWeightMax) = calculateHealthyWeightRange(heightCm)
val bmiPrime = calculateBmiPrime(bmi)
val ponderalIndex = calculatePonderalIndex(heightCm, weightKg)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = stringResource(R.string.use_usc))
Switch(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
checked = roundUp,
onCheckedChange = onRoundUpChanged
Text(
text = stringResource(R.string.result_title),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.bmi_value_category, bmi, category),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(24.dp))
BmiGaugeChart(bmi = bmi)
Spacer(modifier = Modifier.height(24.dp))
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) {
ResultInfoLine(label = stringResource(R.string.healthy_bmi_range))
ResultInfoLine(label = stringResource(R.string.healthy_weight_for_height, healthyWeightMin, healthyWeightMax))
ResultInfoLine(label = stringResource(R.string.bmi_prime, bmiPrime))
ResultInfoLine(label = stringResource(R.string.ponderal_index, ponderalIndex))
}
}
}
@Composable
fun ResultInfoLine(label: String) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "", color = MaterialTheme.colorScheme.onSurface)
Text(text = label, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface)
}
Spacer(modifier = Modifier.height(4.dp))
}
@Composable
fun BmiGaugeChart(bmi: Double) {
val bmiMin = 16.0
val bmiMax = 40.0
val colors = listOf(
Color(0xFFD32F2F), // Underweight
Color(0xFF388E3C), // Normal
Color(0xFFFBC02D), // Overweight
Color(0xFFC62828) // Obesity
)
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(280.dp)) {
Canvas(modifier = Modifier.fillMaxSize()) {
val strokeWidth = 30.dp.toPx()
val center = Offset(size.width / 2, size.height / 2)
val radius = (size.minDimension / 2) - strokeWidth
val scale = 180f / (bmiMax - bmiMin).toFloat()
val underweightSweep = (18.5f - bmiMin.toFloat()) * scale
val normalSweep = (25f - 18.5f) * scale
val overweightSweep = (30f - 25f) * scale
val obesitySweep = (bmiMax.toFloat() - 30f) * scale
drawArc(colors[0], 180f, underweightSweep, false, style = Stroke(width = strokeWidth))
drawArc(colors[1], 180f + underweightSweep, normalSweep, false, style = Stroke(width = strokeWidth))
drawArc(colors[2], 180f + underweightSweep + normalSweep, overweightSweep, false, style = Stroke(width = strokeWidth))
drawArc(colors[3], 180f + underweightSweep + normalSweep + overweightSweep, obesitySweep, false, style = Stroke(width = strokeWidth))
val angle = 180 + (180 * ((bmi.coerceIn(bmiMin, bmiMax) - bmiMin) / (bmiMax - bmiMin))).toFloat()
val needleLength = radius - 10.dp.toPx()
val needleEndX = center.x + needleLength * cos(Math.toRadians(angle.toDouble())).toFloat()
val needleEndY = center.y + needleLength * sin(Math.toRadians(angle.toDouble())).toFloat()
drawLine(onSurfaceColor, center, Offset(needleEndX, needleEndY), 3.dp.toPx(), StrokeCap.Round)
drawCircle(onSurfaceColor, 5.dp.toPx(), center)
}
Text(
text = "BMI = %.1f".format(bmi),
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
color = onSurfaceColor
)
}
}
/**
* 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)
private fun calculateBMI(heightCm: Double, weightKg: Double): Double {
if (heightCm <= 0 || weightKg <= 0) return 0.0
val heightInMeters = heightCm / 100.0
return weightKg / (heightInMeters * heightInMeters)
}
/**
* 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)
private fun getBmiCategory(bmi: Double): String {
return when {
bmi < 18.5 -> "Underweight"
bmi < 25 -> "Normal"
bmi < 30 -> "Overweight"
else -> "Obese"
}
return NumberFormat.getNumberInstance().format(bmi)
}
@Preview(showBackground = true)
private fun calculateHealthyWeightRange(heightCm: Double): Pair<Double, Double> {
if (heightCm <= 0) return 0.0 to 0.0
val heightInMeters = heightCm / 100.0
val minWeight = 18.5 * heightInMeters * heightInMeters
val maxWeight = 24.9 * heightInMeters * heightInMeters
return minWeight to maxWeight
}
private fun calculateBmiPrime(bmi: Double): Double {
return if (bmi > 0) bmi / 25.0 else 0.0
}
private fun calculatePonderalIndex(heightCm: Double, weightKg: Double): Double {
if (heightCm <= 0 || weightKg <= 0) return 0.0
val heightInMeters = heightCm / 100.0
return weightKg / (heightInMeters * heightInMeters * heightInMeters)
}
@Preview(showBackground = true, widthDp = 380)
@Composable
fun TipTimeLayoutPreview() {
fun BmiCalculatorScreenPreview() {
TipTimeTheme {
TipTimeLayout()
BmiCalculatorScreen()
}
}

View File

@ -1,78 +1,71 @@
/*
* 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)
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_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_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFFFD9E2)
val md_theme_light_onTertiaryContainer = Color(0xFF3E001D)
// Custom Army Green & Brown Palette
val armyGreen = Color(0xFF4B5320)
val darkBrown = Color(0xFF5C4033)
val lightCream = Color(0xFFF5F5DC)
val tan = Color(0xFFD2B48C)
// Material 3 Light Theme Colors
val md_theme_light_primary = armyGreen
val md_theme_light_onPrimary = lightCream
val md_theme_light_primaryContainer = tan
val md_theme_light_onPrimaryContainer = darkBrown
val md_theme_light_secondary = darkBrown
val md_theme_light_onSecondary = lightCream
val md_theme_light_secondaryContainer = tan
val md_theme_light_onSecondaryContainer = darkBrown
val md_theme_light_tertiary = tan
val md_theme_light_onTertiary = darkBrown
val md_theme_light_tertiaryContainer = armyGreen
val md_theme_light_onTertiaryContainer = lightCream
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
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_background = lightCream
val md_theme_light_onBackground = darkBrown
val md_theme_light_surface = lightCream
val md_theme_light_onSurface = darkBrown
val md_theme_light_surfaceVariant = tan
val md_theme_light_onSurfaceVariant = darkBrown
val md_theme_light_outline = armyGreen
val md_theme_light_inverseOnSurface = lightCream
val md_theme_light_inverseSurface = darkBrown
val md_theme_light_inversePrimary = lightCream
val md_theme_light_surfaceTint = armyGreen
val md_theme_light_outlineVariant = tan
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)
// Material 3 Dark Theme Colors
val md_theme_dark_primary = tan
val md_theme_dark_onPrimary = darkBrown
val md_theme_dark_primaryContainer = armyGreen
val md_theme_dark_onPrimaryContainer = lightCream
val md_theme_dark_secondary = tan
val md_theme_dark_onSecondary = darkBrown
val md_theme_dark_secondaryContainer = armyGreen
val md_theme_dark_onSecondaryContainer = lightCream
val md_theme_dark_tertiary = lightCream
val md_theme_dark_onTertiary = armyGreen
val md_theme_dark_tertiaryContainer = darkBrown
val md_theme_dark_onTertiaryContainer = lightCream
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)
val md_theme_dark_background = darkBrown
val md_theme_dark_onBackground = lightCream
val md_theme_dark_surface = darkBrown
val md_theme_dark_onSurface = lightCream
val md_theme_dark_surfaceVariant = armyGreen
val md_theme_dark_onSurfaceVariant = lightCream
val md_theme_dark_outline = tan
val md_theme_dark_inverseOnSurface = darkBrown
val md_theme_dark_inverseSurface = lightCream
val md_theme_dark_inversePrimary = armyGreen
val md_theme_dark_surfaceTint = tan
val md_theme_dark_outlineVariant = armyGreen
val md_theme_dark_scrim = Color(0xFF000000)

View File

@ -1,25 +1,22 @@
<?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="calculate_tip">BMI Calculator</string>
<string name="height">Tinggi Badan (cm)</string>
<string name="weight">Berat Badan (kg)</string>
<string name="age">Umur</string>
<!-- Result Screen -->
<string name="result_title">Result</string>
<string name="bmi_value_category">BMI = %.1f kg/m² (%s)</string>
<string name="healthy_bmi_range">Healthy BMI range: 18.5 kg/m² - 25 kg/m²</string>
<string name="healthy_weight_for_height">Healthy weight for your height: %.1f kg - %.1f kg</string>
<string name="bmi_prime">BMI Prime: %.2f</string>
<string name="ponderal_index">Ponderal Index: %.1f kg/m³</string>
<!-- BMI Categories -->
<string name="underweight">Underweight</string>
<string name="normal">Normal</string>
<string name="overweight">Overweight</string>
<string name="obesity">Obesity</string>
</resources>