- Logo
- Logo Male, Female
- Sedikit Perubahan UI/UX
This commit is contained in:
202310715312 HADI GUNA PRAKOSO 2025-11-07 22:02:39 +07:00
parent fa6c42e166
commit 78002b2500
24 changed files with 186 additions and 146 deletions

View File

@ -0,0 +1,72 @@
package com.example.tiptime
import kotlin.math.pow
// File ini berisi semua logika bisnis dan kalkulasi untuk aplikasi BMI.
/**
* Data class untuk menampung semua hasil kalkulasi BMI.
*/
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
)
/**
* Menghitung semua nilai terkait BMI berdasarkan berat, tinggi, dan sistem unit.
* @param weight Berat badan pengguna (dalam kg atau lbs).
* @param height Tinggi badan pengguna (dalam cm atau in).
* @param useMetric `true` jika menggunakan sistem Metrik, `false` jika USC.
* @return `BmiFullResult` jika input valid, atau `null` jika tidak.
*/
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
)
}
/**
* Menentukan kategori BMI berdasarkan nilainya.
*/
fun determineBmiCategory(bmi: Double): String {
return when {
bmi < 18.5 -> "Berat badan kurang"
bmi < 25 -> "Normal"
bmi < 30 -> "Berat badan lebih"
else -> "Obesitas"
}
}

View File

@ -9,35 +9,39 @@ import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.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.MonitorWeight
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.draw.clip
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.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.tiptime.ui.theme.TipTimeTheme
import java.util.Locale
// --- PERBAIKAN DI SINI: Menambahkan kembali import yang hilang ---
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
class MainActivity : ComponentActivity() {
@ -53,15 +57,6 @@ class MainActivity : ComponentActivity() {
}
}
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
)
@Composable
fun BmiCalculatorScreen() {
var heightInput by remember { mutableStateOf("") }
@ -76,8 +71,6 @@ fun BmiCalculatorScreen() {
heightInput = ""
weightInput = ""
ageInput = ""
selectedGender = "Laki-laki"
useMetricUnits = true
bmiResult = null
validationError = null
}
@ -89,15 +82,11 @@ fun BmiCalculatorScreen() {
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
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))
Header()
AnimatedVisibility(visible = bmiResult != null) {
bmiResult?.let { ResultDisplay(it) }
}
if (bmiResult != null) {
Spacer(Modifier.height(24.dp))
}
@ -115,7 +104,8 @@ fun BmiCalculatorScreen() {
text = it,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 8.dp)
modifier = Modifier.padding(top = 8.dp),
textAlign = TextAlign.Center
)
}
@ -127,24 +117,11 @@ fun BmiCalculatorScreen() {
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
}
height == null || weight == null || age == null -> validationError = "Semua kolom harus diisi dengan angka."
age <= 0 || age > 120 -> validationError = "Umur tidak wajar."
(useMetricUnits && (height <= 50 || height > 270)) || (!useMetricUnits && (height <= 20 || height > 108)) -> validationError = "Tinggi badan tidak wajar."
(useMetricUnits && (weight <= 20 || weight > 500)) || (!useMetricUnits && (weight <= 45 || weight > 1100)) -> validationError = "Berat badan tidak wajar."
else -> {
validationError = null
bmiResult = calculateBmi(weight, height, useMetricUnits)
@ -156,6 +133,21 @@ fun BmiCalculatorScreen() {
}
}
@Composable
fun Header() {
Image(
painter = painterResource(id = R.drawable.ic_launcher_playstore),
contentDescription = "Logo Aplikasi",
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
)
Spacer(Modifier.height(16.dp))
Text("Kalkulator BMI", style = MaterialTheme.typography.headlineLarge)
Text("by Hadi Prakoso", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary)
Spacer(Modifier.height(24.dp))
}
@Composable
fun ResultDisplay(result: BmiFullResult) {
Card(
@ -164,13 +156,12 @@ fun ResultDisplay(result: BmiFullResult) {
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)
Text("Hasil", style = MaterialTheme.typography.headlineSmall)
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))
@ -221,8 +212,13 @@ fun BmiGauge(bmi: Double) {
drawCircle(surfaceColor, radius = 5.dp.toPx(), center = center)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = "BMI", style = MaterialTheme.typography.bodyLarge)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom,
modifier = Modifier
.fillMaxSize()
.padding(bottom = 50.dp)
) {
Text(
text = if (bmi > 0) String.format(Locale.getDefault(), "%.1f", bmi) else "-",
style = MaterialTheme.typography.displayMedium.copy(fontWeight = FontWeight.Bold, color = onSurfaceColor)
@ -252,12 +248,18 @@ fun InputPanel(
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)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f))
) {
Column(Modifier.padding(24.dp), 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.MonitorWeight)
}
}
}
@ -288,6 +290,8 @@ fun GenderSelector(selectedOption: String, onOptionSelected: (String) -> Unit) {
val options = listOf("Laki-laki", "Perempuan")
options.forEach { gender ->
val isSelected = selectedOption == gender
val iconRes = if (gender == "Laki-laki") R.drawable.ic_male else R.drawable.ic_female
Button(
onClick = { onOptionSelected(gender) },
shape = RoundedCornerShape(12.dp),
@ -297,7 +301,11 @@ fun GenderSelector(selectedOption: String, onOptionSelected: (String) -> Unit) {
),
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.Person, contentDescription = gender, modifier = Modifier.size(18.dp))
Icon(
painter = painterResource(id = iconRes),
contentDescription = gender,
modifier = Modifier.size(24.dp)
)
Spacer(Modifier.width(8.dp))
Text(gender)
}
@ -318,52 +326,6 @@ fun BmiTextField(label: String, value: String, onValueChange: (String) -> Unit,
)
}
// --- 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"
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Composable
fun BmiCalculatorScreenPreview() {

View File

@ -5,32 +5,42 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
// Mengatur Typography default untuk menggunakan font Poppins
val Typography = Typography(
// Override default text styles to use Poppins
headlineMedium = TextStyle(
headlineLarge = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Bold,
fontSize = 28.sp
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
titleLarge = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
bodyLarge = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
displayMedium = TextStyle(
labelMedium = TextStyle(
fontFamily = Poppins,
fontWeight = FontWeight.Bold,
fontSize = 57.sp
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
/* Anda bisa menambahkan style lain di sini jika diperlukan */
)

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9,18H10M12,18H15M12,13V21M12,13C14.761,13 17,10.761 17,8M12,13C9.239,13 7,10.761 7,8C7,5.239 9.239,3 12,3C13.126,3 14.164,3.372 15,4"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M17,16C17,18.761 14.761,21 12,21C9.239,21 7,18.761 7,16C7,13.239 9.239,11 12,11M12,11C13.126,11 14.164,11.372 15,12M12,11V3L16,7M10,5L8,7"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,22 +1,5 @@
<?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.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -1,22 +1,5 @@
<?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.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#1F7A6F</color>
</resources>