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 {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
@ -21,12 +5,13 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = 35
|
namespace = "com.example.tiptime"
|
||||||
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.example.tiptime"
|
applicationId = "com.example.tiptime"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
@ -46,11 +31,11 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
@ -60,26 +45,23 @@ android {
|
|||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
namespace = "com.example.tiptime"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation(platform("androidx.compose:compose-bom:2024.12.01"))
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||||
implementation("androidx.activity:activity-compose:1.9.3")
|
implementation("androidx.activity:activity-compose:1.8.2")
|
||||||
implementation("androidx.compose.material3:material3")
|
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
||||||
implementation("androidx.compose.ui:ui")
|
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.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.core:core-ktx:1.15.0")
|
implementation("androidx.compose.material3:material3")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
|
implementation("androidx.compose.material:material-icons-extended")
|
||||||
|
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
|
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.compose.ui:ui-test-junit4")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
|
||||||
|
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,214 +1,373 @@
|
|||||||
/*
|
// NAMA: HADI PRAKOSO
|
||||||
* Copyright (C) 2023 The Android Open Source Project
|
// NPM: 2110010323
|
||||||
*
|
|
||||||
* 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
|
package com.example.tiptime
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.annotation.StringRes
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
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.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material.icons.filled.DateRange
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material.icons.filled.LineWeight
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material.icons.filled.SwapVert
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.res.stringResource
|
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.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import com.example.tiptime.ui.theme.TipTimeTheme
|
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() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
TipTimeTheme {
|
TipTimeTheme(darkTheme = true) {
|
||||||
Surface(
|
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
modifier = Modifier.fillMaxSize(),
|
BmiCalculatorScreen()
|
||||||
) {
|
|
||||||
TipTimeLayout()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
@Composable
|
||||||
fun TipTimeLayout() {
|
fun BmiCalculatorScreen() {
|
||||||
var amountInput by remember { mutableStateOf("") }
|
var heightInput by remember { mutableStateOf("") }
|
||||||
var tipInput by remember { mutableStateOf("") }
|
var weightInput by remember { mutableStateOf("") }
|
||||||
var roundUp by remember { mutableStateOf(false) }
|
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) }
|
||||||
|
|
||||||
val BmiHeight = amountInput.toDoubleOrNull() ?: 0.0
|
fun resetFields() {
|
||||||
val BmiWeight = tipInput.toDoubleOrNull() ?: 0.0
|
heightInput = ""
|
||||||
val bmi = calculateBMI(BmiHeight, BmiWeight, roundUp)
|
weightInput = ""
|
||||||
val category = calculateBMICategory(BmiHeight, BmiWeight, roundUp)
|
ageInput = ""
|
||||||
|
selectedGender = "Laki-laki"
|
||||||
|
useMetricUnits = true
|
||||||
|
bmiResult = null
|
||||||
|
validationError = null
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.statusBarsPadding()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 40.dp)
|
.padding(16.dp)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState()),
|
||||||
.safeDrawingPadding(),
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Text("Kalkulator BMI", style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold)
|
||||||
text = stringResource(R.string.calculate_tip),
|
Spacer(Modifier.height(8.dp))
|
||||||
modifier = Modifier
|
Text("by Hadi Prakoso", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
.padding(bottom = 16.dp, top = 40.dp)
|
Spacer(Modifier.height(24.dp))
|
||||||
.align(alignment = Alignment.Start)
|
|
||||||
)
|
AnimatedVisibility(visible = bmiResult != null) {
|
||||||
EditNumberField(
|
bmiResult?.let { ResultDisplay(it) }
|
||||||
label = R.string.height,
|
}
|
||||||
leadingIcon = R.drawable.number,
|
|
||||||
keyboardOptions = KeyboardOptions.Default.copy(
|
if (bmiResult != null) {
|
||||||
keyboardType = KeyboardType.Number,
|
Spacer(Modifier.height(24.dp))
|
||||||
imeAction = ImeAction.Next
|
}
|
||||||
),
|
|
||||||
value = amountInput,
|
InputPanel(
|
||||||
onValueChanged = { amountInput = it },
|
heightInput = heightInput, onHeightChange = { heightInput = it; validationError = null },
|
||||||
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(),
|
weightInput = weightInput, onWeightChange = { weightInput = it; validationError = null },
|
||||||
)
|
ageInput = ageInput, onAgeChange = { ageInput = it; validationError = null },
|
||||||
EditNumberField(
|
selectedGender = selectedGender, onGenderSelect = { selectedGender = it },
|
||||||
label = R.string.weight,
|
useMetric = useMetricUnits, onUnitChange = { useMetricUnits = it }
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
@Composable
|
||||||
fun EditNumberField(
|
fun ResultDisplay(result: BmiFullResult) {
|
||||||
@StringRes label: Int,
|
Card(
|
||||||
@DrawableRes leadingIcon: Int,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
keyboardOptions: KeyboardOptions,
|
shape = RoundedCornerShape(16.dp),
|
||||||
value: String,
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
onValueChanged: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
TextField(
|
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
|
||||||
|
) {
|
||||||
|
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,
|
value = value,
|
||||||
singleLine = true,
|
onValueChange = onValueChange,
|
||||||
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
|
label = { Text(label) },
|
||||||
modifier = modifier,
|
leadingIcon = { Icon(icon, contentDescription = label) },
|
||||||
onValueChange = onValueChanged,
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
|
||||||
label = { Text(stringResource(label)) },
|
shape = RoundedCornerShape(12.dp),
|
||||||
keyboardOptions = keyboardOptions
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
// --- Calculation Logic ---
|
||||||
fun RoundTheTipRow(
|
fun calculateBmi(weight: Double, height: Double, useMetric: Boolean): BmiFullResult? {
|
||||||
roundUp: Boolean,
|
if (height <= 0 || weight <= 0) return null
|
||||||
onRoundUpChanged: (Boolean) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
val bmi = if (useMetric) {
|
||||||
) {
|
val heightInMeters = height / 100
|
||||||
Row(
|
weight / heightInMeters.pow(2)
|
||||||
modifier = modifier.fillMaxWidth(),
|
} else {
|
||||||
verticalAlignment = Alignment.CenterVertically
|
(weight * 703) / height.pow(2)
|
||||||
) {
|
}
|
||||||
Text(text = stringResource(R.string.use_usc))
|
|
||||||
Switch(
|
val category = determineBmiCategory(bmi)
|
||||||
modifier = Modifier
|
val heightInMeters = if (useMetric) height / 100 else height * 0.0254
|
||||||
.fillMaxWidth()
|
val minHealthyWeight = 18.5 * heightInMeters.pow(2)
|
||||||
.wrapContentWidth(Alignment.End),
|
val maxHealthyWeight = 25.0 * heightInMeters.pow(2)
|
||||||
checked = roundUp,
|
|
||||||
onCheckedChange = onRoundUpChanged
|
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)
|
||||||
* 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)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TipTimeLayoutPreview() {
|
fun BmiCalculatorScreenPreview() {
|
||||||
TipTimeTheme {
|
TipTimeTheme(darkTheme = true) {
|
||||||
TipTimeLayout()
|
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
|
package com.example.tiptime.ui.theme
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
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_onPrimary = Color(0xFFFFFFFF)
|
||||||
val md_theme_light_primaryContainer = Color(0xFFFFD9E2)
|
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
|
||||||
val md_theme_light_onPrimaryContainer = Color(0xFF3E001D)
|
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)
|
||||||
val md_theme_light_secondary = Color(0xFF754B9C)
|
val md_theme_light_secondary = Color(0xFF625B71)
|
||||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||||
val md_theme_light_secondaryContainer = Color(0xFFF1DBFF)
|
val md_theme_light_secondaryContainer = Color(0xFFE8DEF8)
|
||||||
val md_theme_light_onSecondaryContainer = Color(0xFF2D0050)
|
val md_theme_light_onSecondaryContainer = Color(0xFF1D192B)
|
||||||
val md_theme_light_tertiary = Color(0xFF984060)
|
val md_theme_light_tertiary = Color(0xFF7D5260)
|
||||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||||
val md_theme_light_tertiaryContainer = Color(0xFFFFD9E2)
|
val md_theme_light_tertiaryContainer = Color(0xFFFFD8E4)
|
||||||
val md_theme_light_onTertiaryContainer = Color(0xFF3E001D)
|
val md_theme_light_onTertiaryContainer = Color(0xFF31111D)
|
||||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
val md_theme_light_error = Color(0xFFB3261E)
|
||||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
|
||||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
val md_theme_light_errorContainer = Color(0xFFF9DEDC)
|
||||||
val md_theme_light_background = Color(0xFFFAFCFF)
|
val md_theme_light_onErrorContainer = Color(0xFF410E0B)
|
||||||
val md_theme_light_onBackground = Color(0xFF001F2A)
|
val md_theme_light_background = Color(0xFFFFFBFE)
|
||||||
val md_theme_light_surface = Color(0xFFFAFCFF)
|
val md_theme_light_onBackground = Color(0xFF1C1B1F)
|
||||||
val md_theme_light_onSurface = Color(0xFF001F2A)
|
val md_theme_light_surface = Color(0xFFFFFBFE)
|
||||||
val md_theme_light_surfaceVariant = Color(0xFFF2DDE2)
|
val md_theme_light_onSurface = Color(0xFF1C1B1F)
|
||||||
val md_theme_light_onSurfaceVariant = Color(0xFF514347)
|
val md_theme_light_surfaceVariant = Color(0xFFE7E0EC)
|
||||||
val md_theme_light_outline = Color(0xFF837377)
|
val md_theme_light_onSurfaceVariant = Color(0xFF49454F)
|
||||||
val md_theme_light_inverseOnSurface = Color(0xFFE1F4FF)
|
val md_theme_light_outline = Color(0xFF79747E)
|
||||||
val md_theme_light_inverseSurface = Color(0xFF003547)
|
val md_theme_light_inverseOnSurface = Color(0xFFF4EFF4)
|
||||||
val md_theme_light_inversePrimary = Color(0xFFFFB0C8)
|
val md_theme_light_inverseSurface = Color(0xFF313033)
|
||||||
val md_theme_light_surfaceTint = Color(0xFF984061)
|
val md_theme_light_inversePrimary = Color(0xFFD0BCFF)
|
||||||
val md_theme_light_outlineVariant = Color(0xFFD5C2C6)
|
val md_theme_light_surfaceTint = Color(0xFF6750A4)
|
||||||
|
val md_theme_light_outlineVariant = Color(0xFFCAC4D0)
|
||||||
val md_theme_light_scrim = Color(0xFF000000)
|
val md_theme_light_scrim = Color(0xFF000000)
|
||||||
|
|
||||||
val md_theme_dark_primary = Color(0xFFFFB0C8)
|
// Dark Theme Colors
|
||||||
val md_theme_dark_onPrimary = Color(0xFF5E1133)
|
val md_theme_dark_primary = Color(0xFFD0BCFF)
|
||||||
val md_theme_dark_primaryContainer = Color(0xFF7B2949)
|
val md_theme_dark_onPrimary = Color(0xFF381E72)
|
||||||
val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9E2)
|
val md_theme_dark_primaryContainer = Color(0xFF4F378B)
|
||||||
val md_theme_dark_secondary = Color(0xFFDEB7FF)
|
val md_theme_dark_onPrimaryContainer = Color(0xFFEADDFF)
|
||||||
val md_theme_dark_onSecondary = Color(0xFF44196A)
|
val md_theme_dark_secondary = Color(0xFFCCC2DC)
|
||||||
val md_theme_dark_secondaryContainer = Color(0xFF5C3382)
|
val md_theme_dark_onSecondary = Color(0xFF332D41)
|
||||||
val md_theme_dark_onSecondaryContainer = Color(0xFFF1DBFF)
|
val md_theme_dark_secondaryContainer = Color(0xFF4A4458)
|
||||||
val md_theme_dark_tertiary = Color(0xFFFFB1C7)
|
val md_theme_dark_onSecondaryContainer = Color(0xFFE8DEF8)
|
||||||
val md_theme_dark_onTertiary = Color(0xFF5E1132)
|
val md_theme_dark_tertiary = Color(0xFFEFB8C8)
|
||||||
val md_theme_dark_tertiaryContainer = Color(0xFF7B2948)
|
val md_theme_dark_onTertiary = Color(0xFF492532)
|
||||||
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E2)
|
val md_theme_dark_tertiaryContainer = Color(0xFF633B48)
|
||||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD8E4)
|
||||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
val md_theme_dark_error = Color(0xFFF2B8B5)
|
||||||
val md_theme_dark_onError = Color(0xFF690005)
|
val md_theme_dark_onError = Color(0xFF601410)
|
||||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
val md_theme_dark_errorContainer = Color(0xFF8C1D18)
|
||||||
val md_theme_dark_background = Color(0xFF001F2A)
|
val md_theme_dark_onErrorContainer = Color(0xFFF9DEDC)
|
||||||
val md_theme_dark_onBackground = Color(0xFFBFE9FF)
|
val md_theme_dark_background = Color(0xFF1C1B1F)
|
||||||
val md_theme_dark_surface = Color(0xFF001F2A)
|
val md_theme_dark_onBackground = Color(0xFFE6E1E5)
|
||||||
val md_theme_dark_onSurface = Color(0xFFBFE9FF)
|
val md_theme_dark_surface = Color(0xFF1C1B1F)
|
||||||
val md_theme_dark_surfaceVariant = Color(0xFF514347)
|
val md_theme_dark_onSurface = Color(0xFFE6E1E5)
|
||||||
val md_theme_dark_onSurfaceVariant = Color(0xFFD5C2C6)
|
val md_theme_dark_surfaceVariant = Color(0xFF49454F)
|
||||||
val md_theme_dark_outline = Color(0xFF9E8C90)
|
val md_theme_dark_onSurfaceVariant = Color(0xFFCAC4D0)
|
||||||
val md_theme_dark_inverseOnSurface = Color(0xFF001F2A)
|
val md_theme_dark_outline = Color(0xFF938F99)
|
||||||
val md_theme_dark_inverseSurface = Color(0xFFBFE9FF)
|
val md_theme_dark_inverseOnSurface = Color(0xFF1C1B1F)
|
||||||
val md_theme_dark_inversePrimary = Color(0xFF984061)
|
val md_theme_dark_inverseSurface = Color(0xFFE6E1E5)
|
||||||
val md_theme_dark_surfaceTint = Color(0xFFFFB0C8)
|
val md_theme_dark_inversePrimary = Color(0xFF6750A4)
|
||||||
val md_theme_dark_outlineVariant = Color(0xFF514347)
|
val md_theme_dark_surfaceTint = Color(0xFFD0BCFF)
|
||||||
|
val md_theme_dark_outlineVariant = Color(0xFF49454F)
|
||||||
val md_theme_dark_scrim = Color(0xFF000000)
|
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
|
package com.example.tiptime.ui.theme
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
|
||||||
import androidx.compose.material3.dynamicLightColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
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(
|
private val LightColorScheme = lightColorScheme(
|
||||||
primary = md_theme_light_primary,
|
primary = md_theme_light_primary,
|
||||||
@ -39,8 +51,8 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||||
error = md_theme_light_error,
|
error = md_theme_light_error,
|
||||||
errorContainer = md_theme_light_errorContainer,
|
|
||||||
onError = md_theme_light_onError,
|
onError = md_theme_light_onError,
|
||||||
|
errorContainer = md_theme_light_errorContainer,
|
||||||
onErrorContainer = md_theme_light_onErrorContainer,
|
onErrorContainer = md_theme_light_onErrorContainer,
|
||||||
background = md_theme_light_background,
|
background = md_theme_light_background,
|
||||||
onBackground = md_theme_light_onBackground,
|
onBackground = md_theme_light_onBackground,
|
||||||
@ -52,60 +64,14 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||||
inverseSurface = md_theme_light_inverseSurface,
|
inverseSurface = md_theme_light_inverseSurface,
|
||||||
inversePrimary = md_theme_light_inversePrimary,
|
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TipTimeTheme(
|
fun TipTimeTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
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
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
val context = LocalContext.current
|
|
||||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
darkTheme -> DarkColorScheme
|
|
||||||
else -> LightColorScheme
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
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
|
package com.example.tiptime.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
// Set of Material typography styles to start with
|
// Set of Material typography styles to start with
|
||||||
val Typography = Typography(
|
val Typography = Typography(
|
||||||
displaySmall = TextStyle(
|
// Override default text styles to use Poppins
|
||||||
fontFamily = FontFamily.Default,
|
headlineMedium = TextStyle(
|
||||||
|
fontFamily = Poppins,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 36.sp,
|
fontSize = 28.sp
|
||||||
lineHeight = 44.sp,
|
),
|
||||||
letterSpacing = 0.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"?>
|
<?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>
|
<resources>
|
||||||
<string name="app_name">BMI Calculator</string>
|
<string name="app_name">BMI Calculator</string>
|
||||||
<string name="calculate_tip">Calculate BMI</string>
|
<string name="bmi_calculator_title">BMI Calculator</string>
|
||||||
<string name="height">Tinggi Badan</string>
|
<string name="age">Age</string>
|
||||||
<string name="weight">Berat Badan</string>
|
<string name="height_m">Height (m/cm)</string>
|
||||||
<string name="use_usc">Gunakan Unit USC (lbs/in)?</string>
|
<string name="height_in">Height (in)</string>
|
||||||
<string name="bmi_calculation">BMI Anda: %s</string>
|
<string name="weight_kg">Weight (kg)</string>
|
||||||
<string name="bmi_category">Kategori: %s</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>
|
</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