Melakukan Update:
- Perubahan Tema - Penambahan Speedometer BMI - Gender - Umur
This commit is contained in:
parent
099c35f19a
commit
9a330b4085
@ -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")
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
11
app/src/main/java/com/example/tiptime/ui/theme/Font.kt
Normal file
11
app/src/main/java/com/example/tiptime/ui/theme/Font.kt
Normal 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)
|
||||
)
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
BIN
app/src/main/res/font/poppins_bold.ttf
Normal file
BIN
app/src/main/res/font/poppins_bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/poppins_regular.ttf
Normal file
BIN
app/src/main/res/font/poppins_regular.ttf
Normal file
Binary file not shown.
@ -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>
|
||||
|
||||
69
app/src/test/java/com/example/tiptime/BmiCalculatorTest.kt
Normal file
69
app/src/test/java/com/example/tiptime/BmiCalculatorTest.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user