Compare commits

..

11 Commits

Author SHA1 Message Date
6120173402 first commit 2025-11-07 20:21:29 +07:00
099c35f19a Update README.md 2025-11-06 12:04:44 +07:00
0ee43c2e9a First update calculation 2025-11-06 11:34:31 +07:00
7053fa6573 First update labels 2025-11-06 11:10:35 +07:00
51c9a8e5ff First commit 2025-11-06 09:58:07 +07:00
renovate[bot]
b029b3dd10
Update all dependencies 8.7.3 to v8.8.0 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-10 05:25:39 +00:00
renovate[bot]
90545a4a4d
Update dependency gradle 8.11.1 to v8.12 (#263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 03:18:17 +00:00
renovate[bot]
e075e6ded8
Update dependency androidx.compose:compose-bom 2024.11.00 to v2024.12.01 (#260)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-12 03:31:35 +00:00
Tomáš Mlynarič
7db6c366a3 Update to kotlin 2.1.0 2024-12-09 17:16:15 +01:00
Jose Alcérreca
076428c67e
Merge pull request #257 from google-developer-training/mlykotom-renovate-fix
Add branches to renovate
2024-12-09 13:44:24 +00:00
Tomáš Mlynarič
59c134aec9
Add branches to renovate 2024-12-09 14:35:04 +01:00
10 changed files with 738 additions and 192 deletions

View File

@ -2,5 +2,10 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>android/.github:renovate-config"
],
"baseBranches": [
"main",
"starter",
"state"
]
}

View File

@ -1,24 +1,50 @@
Tip Time - Solution Code
=================================
# Kalkulator BMI
Solution code for the [Android Basics with Compose](https://developer.android.com/courses/android-basics-compose/course): Tip Time app.
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.
## Deskripsi
Introduction
------------
The Tip Time app contains various UI elements for calculating a tip,
teaching about user input, and State in Compose.
*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.
Pre-requisites
--------------
* Experience with Kotlin syntax.
* How to create and run a project in Android Studio.
## 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.
Getting Started
---------------
1. Install Android Studio, if you don't already have it.
2. Download the sample.
3. Import the sample into Android Studio.
4. Build and run the sample.
## 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

@ -17,6 +17,7 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
android {
@ -45,18 +46,15 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = rootProject.extra["compose_compiler_version"].toString()
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@ -67,7 +65,7 @@ android {
dependencies {
implementation(platform("androidx.compose:compose-bom:2024.11.00"))
implementation(platform("androidx.compose:compose-bom:2024.12.01"))
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
@ -78,7 +76,7 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation(platform("androidx.compose:compose-bom:2024.11.00"))
androidTestImplementation(platform("androidx.compose:compose-bom:2024.12.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")

View File

@ -1,18 +1,15 @@
/*
* Copyright (C) 2023 The Android Open Source Project
* 🌃 Cyberpunk BMI Calculator App
* Developed with by INDRIS ALPASELA
* NPM: [202310715200]
* Nama: INDRIS ALPASELA
*
* 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
* Licensed under the Apache License, Version 2.0
* https://www.apache.org/licenses/LICENSE-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,177 +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 amount = amountInput.toDoubleOrNull() ?: 0.0
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount, tipPercent, roundUp)
Crossfade(targetState = currentScreen, label = "screen_transition") { screen ->
when (screen) {
"home" -> HomeScreen(onNavigateToCalculator = { currentScreen = "calculator" })
"calculator" -> BMICalculatorScreen(onNavigateBack = { currentScreen = "home" })
}
}
}
Column(
// ---------------- 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
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(CyberBlack, CyberDarkBlue, Color(0xFF0D1B2A))
)
)
) {
Text(
text = stringResource(R.string.calculate_tip),
// 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
.padding(bottom = 16.dp, top = 40.dp)
.align(alignment = Alignment.Start)
)
EditNumberField(
label = R.string.bill_amount,
leadingIcon = R.drawable.money,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth(),
)
EditNumberField(
label = R.string.how_was_the_service,
leadingIcon = R.drawable.percent,
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.tip_amount, tip),
style = MaterialTheme.typography.displaySmall
)
Spacer(modifier = Modifier.height(150.dp))
.fillMaxSize()
.statusBarsPadding()
.padding(32.dp)
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Logo/Icon section dengan border neon
Box(
modifier = Modifier
.size(80.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(NeonPink, NeonCyan)
),
shape = RoundedCornerShape(16.dp)
)
.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(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
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)
)
CyberTextField(
label = if (useUSUnits) "WEIGHT (lbs) ⚡" else "BERAT (kg) ⚡",
leadingIcon = R.drawable.number,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
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)
)
// 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
) {
TextField(
value = value,
singleLine = true,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
modifier = modifier,
onValueChange = onValueChanged,
label = { Text(stringResource(label)) },
keyboardOptions = keyboardOptions
)
Column(modifier = modifier) {
TextField(
value = value,
onValueChange = onValueChanged,
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.round_up_tip))
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 ----------------
/**
* Menentukan warna kategori BMI berdasarkan status kesehatan
* @param category String kategori BMI
* @return Color warna yang sesuai dengan kategori
*/
@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
}
/**
* Calculates the tip based on the user input and format the tip amount
* according to the local currency.
* Example would be "$10.00".
* 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 calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
var tip = tipPercent / 100 * amount
if (roundUp) {
tip = kotlin.math.ceil(tip)
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.getCurrencyInstance().format(tip)
// 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 HomeScreenPreview() {
TipTimeTheme { HomeScreen(onNavigateToCalculator = {}) }
}
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
}
}
fun BMICalculatorScreenPreview() {
TipTimeTheme { BMICalculatorScreen(onNavigateBack = {}) }
}

View File

@ -1,23 +0,0 @@
<!--
~ 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.
-->
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7.5,11C9.43,11 11,9.43 11,7.5S9.43,4 7.5,4S4,5.57 4,7.5S5.57,11 7.5,11zM7.5,6C8.33,6 9,6.67 9,7.5S8.33,9 7.5,9S6,8.33 6,7.5S6.67,6 7.5,6z"/>
<path android:fillColor="@android:color/white" android:pathData="M4.0025,18.5831l14.5875,-14.5875l1.4142,1.4142l-14.5875,14.5875z"/>
<path android:fillColor="@android:color/white" android:pathData="M16.5,13c-1.93,0 -3.5,1.57 -3.5,3.5s1.57,3.5 3.5,3.5s3.5,-1.57 3.5,-3.5S18.43,13 16.5,13zM16.5,18c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5S17.33,18 16.5,18z"/>
</vector>

View File

@ -15,10 +15,11 @@
~ limitations under the License.
-->
<resources>
<string name="app_name">Tip Time</string>
<string name="calculate_tip">Calculate Tip</string>
<string name="bill_amount">Bill Amount</string>
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
<string name="tip_amount">Tip Amount: %s</string>
<string name="app_name">BMI Calculator</string>
<string name="calculate_tip">Calculate BMI</string>
<string name="height">Tinggi Badan</string>
<string name="weight">Berat Badan</string>
<string name="use_usc">Gunakan Unit USC (lbs/in)?</string>
<string name="bmi_calculation">BMI Anda: %s</string>
<string name="bmi_category">Kategori: %s</string>
</resources>

View File

@ -14,14 +14,10 @@
* limitations under the License.
*/
buildscript {
extra.apply {
set("compose_compiler_version", "1.5.3")
}
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.7.3" apply false
id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "1.9.10" apply false
id("com.android.application") version "8.13.0" apply false
id("com.android.library") version "8.13.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
}

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

3
gradlew vendored
View File

@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum