first commit

This commit is contained in:
202310715200 INDRIS ALPASELA 2025-11-07 20:21:29 +07:00
parent 099c35f19a
commit 6120173402
2 changed files with 720 additions and 154 deletions

View File

@ -1,10 +1,50 @@
Kalkulator BMI
===============
# Kalkulator BMI
Silahkan kembangkan aplikasi ini untuk melakukan perhitungan BMI
Aplikasi Android sederhana untuk menghitung Indeks Massa Tubuh (IMT) atau *Body Mass Index* (BMI). Aplikasi ini dibuat sebagai bagian dari tugas atau latihan pengembangan aplikasi Android.
Petunjuk lebih detil dapat dibaca di
https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true
## Deskripsi
Starter dimodifikasi dan terinspirasi dari:
https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0
*Body Mass Index* (BMI) atau Indeks Massa Tubuh (IMT) adalah angka yang menjadi standar penilaian untuk menentukan apakah berat badan Anda tergolong normal, kurang, berlebih, atau obesitas. [21] Perhitungan ini didasarkan pada perbandingan antara berat dan tinggi badan. [21] Aplikasi ini menyediakan antarmuka yang mudah digunakan untuk memasukkan data dan melihat hasilnya secara langsung.
Aplikasi ini bertujuan untuk:
* Menyediakan alat praktis bagi pengguna untuk memantau berat badan.
* Membantu pengguna memahami kategori berat badan mereka (kurang, normal, berlebih).
* Menjadi proyek latihan untuk pengembangan aplikasi Android menggunakan teknologi modern.
## Fitur
* **Input Data Pengguna:** Memasukkan berat badan (kg) dan tinggi badan (cm).
* **Perhitungan BMI:** Menghitung skor BMI secara otomatis berdasarkan data yang dimasukkan.
* **Tampilan Hasil:** Menampilkan skor BMI beserta kategori berat badan (contoh: Kurus, Normal, Gemuk).
* **Antarmuka Intuitif:** Desain yang simpel dan mudah digunakan.
## Teknologi
Proyek ini dibangun menggunakan:
* **Kotlin:** Bahasa pemrograman yang direkomendasikan Google untuk pengembangan Android. [8]
* **Jetpack Compose:** *Toolkit* modern dari Google untuk membangun UI Android secara deklaratif, yang memungkinkan pembuatan antarmuka pengguna dengan kode yang lebih ringkas dan efisien. [2, 6, 8]
* **Android Studio:** Lingkungan pengembangan terintegrasi (IDE) resmi untuk pengembangan aplikasi Android. [6]
* **Material Design 3:** Implementasi sistem desain Google untuk memberikan tampilan dan nuansa yang konsisten pada aplikasi. [6]
## Petunjuk Pengembangan
Petunjuk lebih detail mengenai pengembangan dan fitur yang harus diimplementasikan dapat dibaca di dokumen berikut:
[Petunjuk Pengerjaan Proyek](https://docs.google.com/document/d/1iGiC0Bg3Bdcd2Maq45TYkCDUkZ5Ql51E/edit?rtpof=true)
## Cara Berkontribusi
Kontribusi dari Anda sangat diharapkan! Jika Anda ingin berkontribusi pada proyek ini, silakan ikuti langkah-langkah berikut:
1. **Fork** repositori ini ke akun GitHub Anda. [1]
2. **Clone** repositori yang sudah di-fork ke mesin lokal Anda. [1]
3. Buat **branch** baru untuk setiap fitur atau perbaikan yang akan Anda kerjakan (`git checkout -b nama-fitur-anda`). [4]
4. Lakukan perubahan pada kode.
5. **Commit** perubahan Anda dengan pesan yang jelas dan deskriptif (`git commit -m 'Menambahkan fitur X'`). [4]
6. **Push** perubahan ke branch Anda di repositori fork (`git push origin nama-fitur-anda`). [4]
7. Buat **Pull Request** dari branch Anda ke branch `main` repositori ini. [3]
Pastikan untuk membaca aturan kontribusi (jika ada) sebelum memulai. [1]
## Inspirasi
Proyek starter ini dimodifikasi dan terinspirasi dari codelab resmi Android:
[Basic Android Compose - Calculate Tip](https://developer.android.com/codelabs/basic-android-compose-calculate-tip#0)

View File

@ -1,18 +1,15 @@
/*
* 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
* 🌃 Cyberpunk BMI Calculator App
* Developed with by INDRIS ALPASELA
* NPM: [202310715200]
* Nama: INDRIS ALPASELA
*
* Licensed under the Apache License, Version 2.0
* 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.
* Modern BMI calculator with futuristic Cyberpunk Neon theme!
*/
package com.example.tiptime
import android.os.Bundle
@ -20,195 +17,724 @@ 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.Crossfade
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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 kotlin.math.pow
import kotlin.math.sin
// ---------------- CYBERPUNK NEON COLORS ----------------
val NeonPink = Color(0xFFFF006E)
val NeonCyan = Color(0xFF00F5FF)
val NeonPurple = Color(0xFF8B00FF)
val NeonYellow = Color(0xFFFFFB00)
val CyberBlack = Color(0xFF0A0E27)
val CyberDarkBlue = Color(0xFF1A1F3A)
val NeonGreen = Color(0xFF39FF14)
val NeonOrange = Color(0xFFFF3C00)
// ---------------- MAIN ACTIVITY ----------------
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
TipTimeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
) {
TipTimeLayout()
Surface(modifier = Modifier.fillMaxSize()) {
BMIApp()
}
}
}
}
}
// ---------------- APP NAVIGATION ----------------
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
var roundUp by remember { mutableStateOf(false) }
fun BMIApp() {
var currentScreen by remember { mutableStateOf("home") }
val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0
val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0
val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp)
val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp)
Crossfade(targetState = currentScreen, label = "screen_transition") { screen ->
when (screen) {
"home" -> HomeScreen(onNavigateToCalculator = { currentScreen = "calculator" })
"calculator" -> BMICalculatorScreen(onNavigateBack = { currentScreen = "home" })
}
}
}
// ---------------- HOME SCREEN WITH ANIMATED NEON EFFECT ----------------
@Composable
fun HomeScreen(onNavigateToCalculator: () -> Unit) {
// Animasi untuk efek neon berkedip
val infiniteTransition = rememberInfiniteTransition(label = "neon_pulse")
val glowAlpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 0.8f,
animationSpec = infiniteRepeatable(
animation = tween(1500, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "glow_animation"
)
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(CyberBlack, CyberDarkBlue, Color(0xFF0D1B2A))
)
)
) {
// Grid lines untuk efek cyberpunk
Canvas(modifier = Modifier.fillMaxSize()) {
val gridSize = 60f
// Vertical lines
var x = 0f
while (x < size.width) {
drawLine(
color = NeonCyan.copy(alpha = 0.1f),
start = Offset(x, 0f),
end = Offset(x, size.height),
strokeWidth = 1f
)
x += gridSize
}
// Horizontal lines
var y = 0f
while (y < size.height) {
drawLine(
color = NeonPink.copy(alpha = 0.1f),
start = Offset(0f, y),
end = Offset(size.width, y),
strokeWidth = 1f
)
y += gridSize
}
// Neon circles dengan efek glow
drawCircle(
color = NeonPink.copy(alpha = glowAlpha * 0.2f),
radius = 200f,
center = Offset(x = size.width * 0.2f, y = size.height * 0.3f)
)
drawCircle(
color = NeonCyan.copy(alpha = glowAlpha * 0.15f),
radius = 250f,
center = Offset(x = size.width * 0.8f, y = size.height * 0.6f)
)
drawCircle(
color = NeonPurple.copy(alpha = glowAlpha * 0.18f),
radius = 180f,
center = Offset(x = size.width * 0.5f, y = size.height * 0.8f)
)
}
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.padding(32.dp)
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
// Logo/Icon section dengan border neon
Box(
modifier = Modifier
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
.size(80.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(NeonPink, NeonCyan)
),
shape = RoundedCornerShape(16.dp)
)
EditNumberField(
label = R.string.height,
.padding(2.dp)
.background(CyberBlack, RoundedCornerShape(14.dp)),
contentAlignment = Alignment.Center
) {
Text(
text = "",
fontSize = 40.sp,
color = NeonCyan
)
}
Spacer(Modifier.height(24.dp))
Text(
text = "CYBER BMI",
style = MaterialTheme.typography.displayLarge.copy(
fontWeight = FontWeight.Black,
brush = Brush.linearGradient(
colors = listOf(NeonPink, NeonCyan, NeonPurple)
)
),
textAlign = TextAlign.Center
)
Text(
text = "// CALCULATOR_V2.0",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Light,
letterSpacing = 4.sp
),
color = NeonCyan.copy(alpha = 0.7f),
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 8.dp)
)
Text(
text = "< Developed by Faris Naufal />",
style = MaterialTheme.typography.bodySmall.copy(
fontWeight = FontWeight.Light
),
color = Color.White.copy(alpha = 0.5f),
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 4.dp)
)
Spacer(Modifier.height(16.dp))
// Deskripsi dengan border cyberpunk
Card(
colors = CardDefaults.cardColors(
containerColor = CyberDarkBlue.copy(alpha = 0.6f)
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(
text = "Hitung Body Mass Index Anda dengan teknologi futuristik",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = Color.White.copy(alpha = 0.9f),
modifier = Modifier.padding(16.dp)
)
}
Spacer(Modifier.height(48.dp))
// Button dengan efek neon gradient
Button(
onClick = onNavigateToCalculator,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
contentPadding = PaddingValues(0.dp),
shape = RoundedCornerShape(12.dp)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.horizontalGradient(
colors = listOf(NeonPink, NeonPurple, NeonCyan)
)
),
contentAlignment = Alignment.Center
) {
Text(
"⚡ MULAI SCANNING",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
),
color = Color.White
)
}
}
}
}
}
// ---------------- BMI CALCULATOR SCREEN ----------------
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BMICalculatorScreen(onNavigateBack: () -> Unit) {
var heightInput by remember { mutableStateOf("") }
var weightInput by remember { mutableStateOf("") }
var useUSUnits by remember { mutableStateOf(false) }
// Validasi input untuk mencegah nilai yang tidak wajar
val height = heightInput.toDoubleOrNull() ?: 0.0
val weight = weightInput.toDoubleOrNull() ?: 0.0
// Validasi range yang wajar
val isValidHeight = if (useUSUnits) height in 20.0..96.0 else height in 50.0..250.0
val isValidWeight = if (useUSUnits) weight in 20.0..1000.0 else weight in 10.0..500.0
val bmi = calculateBMI(height, weight, useUSUnits)
val category = calculateBMICategory(height, weight, useUSUnits)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"⚡ CYBER BMI SCANNER",
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp
)
)
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Kembali",
tint = NeonCyan
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = CyberBlack,
titleContentColor = NeonPink
)
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(CyberBlack, CyberDarkBlue)
)
)
.padding(innerPadding)
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
// Unit Switch dengan desain cyberpunk
UnitSwitchRow(
useUSUnits = useUSUnits,
onUnitChanged = { useUSUnits = it },
modifier = Modifier.padding(bottom = 24.dp)
)
// Input fields dengan neon border
CyberTextField(
label = if (useUSUnits) "HEIGHT (inches) ⚡" else "TINGGI (cm) ⚡",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(),
value = heightInput,
onValueChanged = { heightInput = it },
isError = height > 0 && !isValidHeight,
errorMessage = if (useUSUnits) "Range: 20-96 inches" else "Range: 50-250 cm",
modifier = Modifier.fillMaxWidth().padding(bottom = 20.dp)
)
EditNumberField(
label = R.string.weight,
CyberTextField(
label = if (useUSUnits) "WEIGHT (lbs) ⚡" else "BERAT (kg) ⚡",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardOptions = KeyboardOptions(
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
value = weightInput,
onValueChanged = { weightInput = it },
isError = weight > 0 && !isValidWeight,
errorMessage = if (useUSUnits) "Range: 20-1000 lbs" else "Range: 10-500 kg",
modifier = Modifier.fillMaxWidth().padding(bottom = 32.dp)
)
Spacer(modifier = Modifier.height(150.dp))
// Tampilkan hasil hanya jika input valid
if (height > 0 && weight > 0) {
if (isValidHeight && isValidWeight) {
// Card hasil BMI dengan efek neon
Card(
colors = CardDefaults.cardColors(
containerColor = CyberDarkBlue.copy(alpha = 0.8f)
),
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.linearGradient(
colors = listOf(
NeonPink.copy(alpha = 0.3f),
NeonCyan.copy(alpha = 0.3f)
)
),
shape = RoundedCornerShape(20.dp)
)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "// SCAN RESULT",
style = MaterialTheme.typography.titleSmall.copy(
letterSpacing = 3.sp
),
color = NeonCyan.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = bmi,
style = MaterialTheme.typography.displayLarge.copy(
fontWeight = FontWeight.Black,
brush = Brush.linearGradient(
colors = listOf(
getBMICategoryColor(category),
getBMICategoryColor(category).copy(alpha = 0.7f)
)
)
)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = category.uppercase(),
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
),
color = getBMICategoryColor(category)
)
}
}
Spacer(modifier = Modifier.height(24.dp))
BMICategoryTable()
} else {
// Peringatan untuk input tidak valid
Card(
colors = CardDefaults.cardColors(
containerColor = NeonOrange.copy(alpha = 0.2f)
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "⚠ INPUT TIDAK VALID\nMasukkan nilai yang wajar",
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.SemiBold
),
color = NeonOrange,
textAlign = TextAlign.Center,
modifier = Modifier.padding(16.dp)
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
// ---------------- BMI CATEGORY TABLE WITH CYBERPUNK STYLE ----------------
@Composable
fun BMICategoryTable() {
Card(
colors = CardDefaults.cardColors(
containerColor = CyberDarkBlue.copy(alpha = 0.7f)
),
shape = RoundedCornerShape(16.dp),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "// BMI CATEGORIES",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 2.sp
),
color = NeonCyan,
modifier = Modifier.padding(bottom = 12.dp)
)
Divider(
color = NeonPink.copy(alpha = 0.5f),
thickness = 2.dp,
modifier = Modifier.padding(bottom = 12.dp)
)
BMICategoryRow("UNDERWEIGHT", "< 18.5", Color(0xFF00D9FF))
Spacer(modifier = Modifier.height(8.dp))
BMICategoryRow("NORMAL", "18.5 24.9", NeonGreen)
Spacer(modifier = Modifier.height(8.dp))
BMICategoryRow("OVERWEIGHT", "25 29.9", NeonYellow)
Spacer(modifier = Modifier.height(8.dp))
BMICategoryRow("OBESE", "≥ 30", NeonOrange)
}
}
}
@Composable
fun EditNumberField(
@StringRes label: Int,
fun BMICategoryRow(label: String, range: String, color: Color) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(
colors = listOf(
color.copy(alpha = 0.3f),
color.copy(alpha = 0.1f)
)
),
shape = RoundedCornerShape(8.dp)
)
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = label,
color = color,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = range,
color = color,
fontSize = 14.sp,
fontWeight = FontWeight.SemiBold
)
}
}
// ---------------- CUSTOM CYBERPUNK TEXT FIELD ----------------
@Composable
fun CyberTextField(
label: String,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
onValueChanged: (String) -> Unit,
isError: Boolean = false,
errorMessage: String = "",
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
TextField(
value = value,
singleLine = true,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
modifier = modifier,
onValueChange = onValueChanged,
label = { Text(stringResource(label)) },
keyboardOptions = keyboardOptions
singleLine = true,
leadingIcon = {
Icon(
painterResource(id = leadingIcon),
contentDescription = null,
tint = if (isError) NeonOrange else NeonCyan
)
},
label = {
Text(
label,
color = if (isError) NeonOrange else NeonPink,
style = MaterialTheme.typography.bodyMedium.copy(
letterSpacing = 1.sp
)
)
},
placeholder = {
Text(
"Enter value...",
color = Color.Gray.copy(alpha = 0.5f)
)
},
keyboardOptions = keyboardOptions,
isError = isError,
colors = TextFieldDefaults.colors(
focusedContainerColor = CyberDarkBlue.copy(alpha = 0.6f),
unfocusedContainerColor = CyberDarkBlue.copy(alpha = 0.4f),
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedIndicatorColor = if (isError) NeonOrange else NeonCyan,
unfocusedIndicatorColor = if (isError) NeonOrange.copy(alpha = 0.5f) else NeonPurple.copy(alpha = 0.5f),
errorIndicatorColor = NeonOrange,
errorContainerColor = CyberDarkBlue.copy(alpha = 0.6f),
cursorColor = NeonCyan
),
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
if (isError && errorMessage.isNotEmpty()) {
Text(
text = errorMessage,
color = NeonOrange,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
}
}
// ---------------- UNIT SWITCH WITH CYBERPUNK STYLE ----------------
@Composable
fun RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
fun UnitSwitchRow(
useUSUnits: Boolean,
onUnitChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
Card(
colors = CardDefaults.cardColors(
containerColor = CyberDarkBlue.copy(alpha = 0.6f)
),
shape = RoundedCornerShape(12.dp),
modifier = modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.use_usc))
Switch(
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
checked = roundUp,
onCheckedChange = onRoundUpChanged
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = if (useUSUnits) "US UNITS" else "METRIC UNITS",
color = NeonCyan,
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
letterSpacing = 1.sp
)
)
Text(
text = if (useUSUnits) "inches, lbs" else "cm, kg",
color = Color.White.copy(alpha = 0.6f),
style = MaterialTheme.typography.bodySmall
)
}
Switch(
checked = useUSUnits,
onCheckedChange = onUnitChanged,
colors = SwitchDefaults.colors(
checkedThumbColor = NeonCyan,
checkedTrackColor = NeonPink.copy(alpha = 0.5f),
uncheckedThumbColor = Color.Gray,
uncheckedTrackColor = CyberBlack
)
)
}
}
}
// ---------------- LOGIC & UTILITIES ----------------
/**
* Calculates the BMI
*
* Catatan: tambahkan unit test untuk kalkulasi BMI ini
* Menentukan warna kategori BMI berdasarkan status kesehatan
* @param category String kategori BMI
* @return Color warna yang sesuai dengan kategori
*/
private fun calculateBMI(BmiHeight: Double, BmiWeight: Double = 15.0, roundUp: Boolean): String {
var bmi = BmiWeight / 100 * BmiHeight
if (roundUp) {
bmi = kotlin.math.ceil(bmi)
@Composable
fun getBMICategoryColor(category: String): Color = when {
category.contains("Underweight", true) || category.contains("Kurus", true) -> Color(0xFF00D9FF)
category.contains("Normal", true) -> NeonGreen
category.contains("Overweight", true) || category.contains("Gemuk", true) -> NeonYellow
category.contains("Obese", true) || category.contains("Obesitas", true) -> NeonOrange
else -> Color.White
}
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)
/**
* Menghitung nilai BMI berdasarkan tinggi dan berat
* Formula Metrik: BMI = weight (kg) / (height (m))^2
* Formula US: BMI = (weight (lbs) / (height (inches))^2) * 703
*
* @param height Tinggi badan (cm untuk metrik, inches untuk US)
* @param weight Berat badan (kg untuk metrik, lbs untuk US)
* @param useUSUnits Boolean untuk menentukan sistem satuan
* @return String nilai BMI dengan 1 desimal
*/
private fun calculateBMI(height: Double, weight: Double, useUSUnits: Boolean): String {
// Validasi input tidak boleh nol atau negatif
if (height <= 0 || weight <= 0) return "-"
val bmi = if (useUSUnits) {
// Formula US: (weight / height^2) * 703
(weight / height.pow(2)) * 703
} else {
// Formula Metrik: weight / (height in meters)^2
weight / (height / 100).pow(2)
}
return NumberFormat.getNumberInstance().format(bmi)
// Format dengan 1 angka desimal
return String.format("%.1f", bmi)
}
/**
* Menentukan kategori BMI berdasarkan standar WHO
* - Underweight: BMI < 18.5
* - Normal: BMI 18.5-24.9
* - Overweight: BMI 25-29.9
* - Obese: BMI >= 30
*
* @param height Tinggi badan
* @param weight Berat badan
* @param useUSUnits Boolean sistem satuan
* @return String kategori BMI
*/
private fun calculateBMICategory(height: Double, weight: Double, useUSUnits: Boolean): String {
if (height <= 0 || weight <= 0) return "-"
val bmi = if (useUSUnits) {
(weight / height.pow(2)) * 703
} else {
weight / (height / 100).pow(2)
}
return when {
bmi < 18.5 -> "Underweight (Kurus)"
bmi < 25 -> "Normal"
bmi < 30 -> "Overweight (Gemuk)"
else -> "Obese (Obesitas)"
}
}
// ---------------- PREVIEWS ----------------
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
fun HomeScreenPreview() {
TipTimeTheme { HomeScreen(onNavigateToCalculator = {}) }
}
@Preview(showBackground = true)
@Composable
fun BMICalculatorScreenPreview() {
TipTimeTheme { BMICalculatorScreen(onNavigateBack = {}) }
}