Compare commits
10 Commits
b029b3dd10
...
f921fd2919
| Author | SHA1 | Date | |
|---|---|---|---|
| f921fd2919 | |||
| b7ed4718eb | |||
| 2dfd0f3f6e | |||
| 39959cbb9b | |||
| bae6759c9d | |||
| de59cc8f47 | |||
| 099c35f19a | |||
| 0ee43c2e9a | |||
| 7053fa6573 | |||
| 51c9a8e5ff |
102
README.md
102
README.md
@ -1,24 +1,92 @@
|
||||
Tip Time - Solution Code
|
||||
=================================
|
||||
# Kalkulator BMI Modern
|
||||
|
||||
Solution code for the [Android Basics with Compose](https://developer.android.com/courses/android-basics-compose/course): Tip Time app.
|
||||
Sebuah aplikasi Android modern dan elegan untuk menghitung Indeks Massa Tubuh (BMI), dibangun sepenuhnya menggunakan Jetpack Compose. Proyek ini dikembangkan oleh **Fazri Abdurrahman (202310715082)** dengan kolaborasi intensif bersama AI (Gemini) untuk menerapkan fungsionalitas canggih, desain yang menarik, dan praktik pengembangan modern.
|
||||
|
||||
---
|
||||
|
||||
Introduction
|
||||
------------
|
||||
The Tip Time app contains various UI elements for calculating a tip,
|
||||
teaching about user input, and State in Compose.
|
||||
## 📖 Daftar Isi
|
||||
|
||||
- [Fitur Utama](#-fitur-utama)
|
||||
- [Tangkapan Layar](#-tangkapan-layar)
|
||||
- [Teknologi yang Digunakan](#️-teknologi-yang-digunakan)
|
||||
- [Instalasi & Penggunaan](#-instalasi--penggunaan)
|
||||
- [Proses Pengembangan dengan AI](#-proses-pengembangan-dengan-ai)
|
||||
- [Rencana Pengembangan](#-rencana-pengembangan)
|
||||
- [Lisensi](#-lisensi)
|
||||
|
||||
Pre-requisites
|
||||
--------------
|
||||
* Experience with Kotlin syntax.
|
||||
* How to create and run a project in Android Studio.
|
||||
---
|
||||
|
||||
## ✨ Fitur Utama
|
||||
|
||||
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.
|
||||
- **Input Data Intuitif:** Form input yang jelas untuk **Tinggi (cm)**, **Berat (kg)**, dan **Umur**.
|
||||
- **Visualisasi BMI Real-time:** Hasil BMI ditampilkan secara dinamis melalui **Gauge Chart (Speedometer)** yang interaktif, memberikan umpan balik visual yang langsung dipahami.
|
||||
- **Analisis Hasil Komprehensif:**
|
||||
- Nilai BMI akurat beserta **kategori** (Kurus, Normal, Gemuk, Obesitas).
|
||||
- Informasi **rentang berat badan ideal** sesuai tinggi badan.
|
||||
- Kalkulasi metrik tambahan seperti **BMI Prime** dan **Ponderal Index**.
|
||||
- **Desain Modern & Responsif:** Antarmuka pengguna yang bersih dengan palet warna custom (hijau army & coklat) yang diimplementasikan menggunakan **Material 3 Theming**.
|
||||
|
||||
---
|
||||
|
||||
## 📸 Tangkapan Layar
|
||||
|
||||
*(Sangat disarankan untuk menambahkan gambar pratinjau aplikasi Anda di sini untuk menunjukkan hasil akhirnya. Ini memberikan kesan pertama yang kuat.)*
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Teknologi yang Digunakan
|
||||
|
||||
Proyek ini dibangun menggunakan tumpukan teknologi Android modern:
|
||||
|
||||
- **Bahasa Pemrograman:** **Kotlin** - Bahasa utama untuk pengembangan Android, dipilih karena keringkasan, keamanan, dan fitur-fitur modernnya.
|
||||
- **UI Toolkit:** **Jetpack Compose** - Membangun seluruh UI secara deklaratif, memungkinkan pengembangan yang lebih cepat dan kode yang lebih mudah dikelola.
|
||||
- **Desain Sistem:** **Material 3** - Mengimplementasikan sistem desain terbaru dari Google untuk memastikan tampilan yang konsisten dan modern.
|
||||
- **Manajemen State:** State management sederhana namun kuat menggunakan `remember` dan `mutableStateOf` untuk menjaga UI tetap sinkron dengan data.
|
||||
- **Grafik Kustom:** Pemanfaatan **`Canvas` API** untuk menggambar komponen UI yang sepenuhnya custom (Gauge Chart) dari awal.
|
||||
- **IDE:** Android Studio
|
||||
- **Version Control:** Git
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Instalasi & Penggunaan
|
||||
|
||||
1. **Prasyarat:** Pastikan Anda memiliki versi terbaru **Android Studio**.
|
||||
2. **Clone Repositori:**
|
||||
```sh
|
||||
git clone [URL-repositori-Anda-di-sini]
|
||||
```
|
||||
3. **Buka di Android Studio:** Buka proyek yang telah di-clone.
|
||||
4. **Sync Gradle:** Tunggu hingga Android Studio selesai mengunduh semua dependensi yang diperlukan.
|
||||
5. **Jalankan Aplikasi:** Pilih target (Emulator atau perangkat fisik) dan klik tombol 'Run'.
|
||||
|
||||
---
|
||||
|
||||
## 🤖 Proses Pengembangan dengan AI
|
||||
|
||||
Proyek ini menjadi studi kasus menarik dalam **pengembangan perangkat lunak berbantuan AI**. Bermula dari sebuah *starter code* yang sangat dasar, sebagian besar fungsionalitas dan polesan aplikasi ini diimplementasikan melalui dialog dan iterasi dengan **Google Gemini**.
|
||||
|
||||
Prosesnya meliputi:
|
||||
1. **Prompting Fitur:** Meminta implementasi fitur dari nol, seperti Gauge Chart.
|
||||
2. **Debugging Kolaboratif:** Mengidentifikasi dan memperbaiki bug (termasuk eror pada `Canvas` dan referensi tema yang salah) dengan bantuan analisis dari AI.
|
||||
3. **Refactoring & Desain:** Meminta perubahan desain, seperti skema warna dan tata letak, yang kemudian diimplementasikan oleh AI.
|
||||
4. **Dokumentasi:** Menghasilkan dokumentasi ini secara bertahap sesuai progres pengembangan.
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Rencana Pengembangan
|
||||
|
||||
Beberapa fitur potensial yang dapat ditambahkan di masa depan:
|
||||
|
||||
- [ ] **Penyimpanan Riwayat:** Menyimpan hasil kalkulasi BMI untuk melacak progres.
|
||||
- [ ] **Grafik Tren:** Visualisasi perubahan BMI dari waktu ke waktu.
|
||||
- [ ] **Dukungan Unit Imperial:** Opsi input tinggi (kaki/inci) dan berat (pon).
|
||||
- [ ] **Lokalisasi:** Mendukung berbagai bahasa.
|
||||
|
||||
---
|
||||
|
||||
## 📄 Lisensi
|
||||
|
||||
Proyek ini dilisensikan di bawah **MIT License**. *(Disarankan untuk membuat file `LICENSE` di root proyek Anda dan menempelkan teks lisensi MIT ke dalamnya)*.
|
||||
|
||||
---
|
||||
*Proyek ini dikembangkan dari starter code sederhana yang terinspirasi dari: Codelab "Calculate Tip" Android Developer.*
|
||||
|
||||
@ -1,18 +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.
|
||||
*/
|
||||
package com.example.tiptime
|
||||
|
||||
import android.os.Bundle
|
||||
@ -21,7 +6,10 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@ -30,17 +18,18 @@ 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.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
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.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
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
|
||||
@ -48,14 +37,20 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.tiptime.ui.theme.TipTimeTheme
|
||||
import java.text.NumberFormat
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@ -65,8 +60,9 @@ class MainActivity : ComponentActivity() {
|
||||
TipTimeTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
TipTimeLayout()
|
||||
BmiCalculatorScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -74,62 +70,60 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TipTimeLayout() {
|
||||
var amountInput by remember { mutableStateOf("") }
|
||||
var tipInput by remember { mutableStateOf("") }
|
||||
var roundUp by remember { mutableStateOf(false) }
|
||||
fun BmiCalculatorScreen() {
|
||||
var heightInput by remember { mutableStateOf("175") } // Default for preview
|
||||
var weightInput by remember { mutableStateOf("70") } // Default for preview
|
||||
var ageInput by remember { mutableStateOf("25") }
|
||||
|
||||
val amount = amountInput.toDoubleOrNull() ?: 0.0
|
||||
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
|
||||
val tip = calculateTip(amount, tipPercent, roundUp)
|
||||
val heightCm = heightInput.toDoubleOrNull() ?: 0.0
|
||||
val weightKg = weightInput.toDoubleOrNull() ?: 0.0
|
||||
|
||||
val bmi = calculateBMI(heightCm, weightKg)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 40.dp)
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.safeDrawingPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.calculate_tip),
|
||||
modifier = Modifier
|
||||
.padding(bottom = 16.dp, top = 40.dp)
|
||||
.align(alignment = Alignment.Start)
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
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(),
|
||||
label = R.string.height,
|
||||
leadingIcon = R.drawable.number,
|
||||
value = heightInput,
|
||||
onValueChanged = { heightInput = it },
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
EditNumberField(
|
||||
label = R.string.how_was_the_service,
|
||||
leadingIcon = R.drawable.percent,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Number,
|
||||
label = R.string.weight,
|
||||
leadingIcon = R.drawable.number,
|
||||
value = weightInput,
|
||||
onValueChanged = { weightInput = it },
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
EditNumberField(
|
||||
label = R.string.age,
|
||||
leadingIcon = R.drawable.number,
|
||||
value = ageInput,
|
||||
onValueChanged = { ageInput = it },
|
||||
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))
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
if (bmi > 0) {
|
||||
ResultScreen(bmi = bmi, heightCm = heightCm, weightKg = weightKg)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,60 +131,183 @@ fun TipTimeLayout() {
|
||||
fun EditNumberField(
|
||||
@StringRes label: Int,
|
||||
@DrawableRes leadingIcon: Int,
|
||||
keyboardOptions: KeyboardOptions,
|
||||
value: String,
|
||||
onValueChanged: (String) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
imeAction: ImeAction = ImeAction.Next
|
||||
) {
|
||||
TextField(
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
singleLine = true,
|
||||
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
|
||||
modifier = modifier,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onValueChange = onValueChanged,
|
||||
label = { Text(stringResource(label)) },
|
||||
keyboardOptions = keyboardOptions
|
||||
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Number,
|
||||
imeAction = imeAction
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RoundTheTipRow(
|
||||
roundUp: Boolean,
|
||||
onRoundUpChanged: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
fun ResultScreen(bmi: Double, heightCm: Double, weightKg: Double) {
|
||||
val category = getBmiCategory(bmi)
|
||||
val (healthyWeightMin, healthyWeightMax) = calculateHealthyWeightRange(heightCm)
|
||||
val bmiPrime = calculateBmiPrime(bmi)
|
||||
val ponderalIndex = calculatePonderalIndex(heightCm, weightKg)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Text(
|
||||
text = stringResource(R.string.result_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.bmi_value_category, bmi, category),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
BmiGaugeChart(bmi = bmi)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f))
|
||||
) {
|
||||
Text(text = stringResource(R.string.round_up_tip))
|
||||
Switch(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.End),
|
||||
checked = roundUp,
|
||||
onCheckedChange = onRoundUpChanged
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
ResultInfoLine(label = stringResource(R.string.healthy_bmi_range))
|
||||
ResultInfoLine(
|
||||
label = stringResource(
|
||||
R.string.healthy_weight_for_height,
|
||||
healthyWeightMin,
|
||||
healthyWeightMax
|
||||
)
|
||||
)
|
||||
ResultInfoLine(label = stringResource(R.string.bmi_prime, bmiPrime))
|
||||
ResultInfoLine(label = stringResource(R.string.ponderal_index, ponderalIndex))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResultInfoLine(label: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "• ",
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BmiGaugeChart(bmi: Double) {
|
||||
val bmiMin = 16.0
|
||||
val bmiMax = 40.0
|
||||
|
||||
val colors = listOf(
|
||||
Color(0xFFD32F2F), // Underweight
|
||||
Color(0xFF388E3C), // Normal
|
||||
Color(0xFFFBC02D), // Overweight
|
||||
Color(0xFFC62828) // Obesity
|
||||
)
|
||||
|
||||
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
|
||||
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(280.dp)) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val strokeWidth = 30.dp.toPx()
|
||||
val center = Offset(size.width / 2, size.height / 2)
|
||||
val radius = (size.minDimension / 2) - strokeWidth
|
||||
|
||||
val scale = 180f / (bmiMax - bmiMin).toFloat()
|
||||
val underweightSweep = (18.5f - bmiMin.toFloat()) * scale
|
||||
val normalSweep = (25f - 18.5f) * scale
|
||||
val overweightSweep = (30f - 25f) * scale
|
||||
val obesitySweep = (bmiMax.toFloat() - 30f) * scale
|
||||
|
||||
drawArc(colors[0], 180f, underweightSweep, false, style = Stroke(width = strokeWidth))
|
||||
drawArc(colors[1], 180f + underweightSweep, normalSweep, false, style = Stroke(width = strokeWidth))
|
||||
drawArc(colors[2], 180f + underweightSweep + normalSweep, overweightSweep, false, style = Stroke(width = strokeWidth))
|
||||
drawArc(colors[3], 180f + underweightSweep + normalSweep + overweightSweep, obesitySweep, false, style = Stroke(width = strokeWidth))
|
||||
|
||||
val angle = 180 + (180 * ((bmi.coerceIn(bmiMin, bmiMax) - bmiMin) / (bmiMax - bmiMin))).toFloat()
|
||||
val needleLength = radius - 10.dp.toPx()
|
||||
val needleEndX = center.x + needleLength * cos(Math.toRadians(angle.toDouble())).toFloat()
|
||||
val needleEndY = center.y + needleLength * sin(Math.toRadians(angle.toDouble())).toFloat()
|
||||
|
||||
drawLine(onSurfaceColor, center, Offset(needleEndX, needleEndY), 3.dp.toPx(), StrokeCap.Round)
|
||||
drawCircle(onSurfaceColor, 5.dp.toPx(), center)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "BMI = %.1f".format(bmi),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = onSurfaceColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tip based on the user input and format the tip amount
|
||||
* according to the local currency.
|
||||
* Example would be "$10.00".
|
||||
*/
|
||||
private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
|
||||
var tip = tipPercent / 100 * amount
|
||||
if (roundUp) {
|
||||
tip = kotlin.math.ceil(tip)
|
||||
}
|
||||
return NumberFormat.getCurrencyInstance().format(tip)
|
||||
private fun calculateBMI(heightCm: Double, weightKg: Double): Double {
|
||||
if (heightCm <= 0 || weightKg <= 0) return 0.0
|
||||
val heightInMeters = heightCm / 100.0
|
||||
return weightKg / (heightInMeters * heightInMeters)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
private fun getBmiCategory(bmi: Double): String {
|
||||
return when {
|
||||
bmi < 18.5 -> "Underweight"
|
||||
bmi < 25 -> "Normal"
|
||||
bmi < 30 -> "Overweight"
|
||||
else -> "Obese"
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateHealthyWeightRange(heightCm: Double): Pair<Double, Double> {
|
||||
if (heightCm <= 0) return 0.0 to 0.0
|
||||
val heightInMeters = heightCm / 100.0
|
||||
val minWeight = 18.5 * heightInMeters * heightInMeters
|
||||
val maxWeight = 24.9 * heightInMeters * heightInMeters
|
||||
return minWeight to maxWeight
|
||||
}
|
||||
|
||||
private fun calculateBmiPrime(bmi: Double): Double {
|
||||
return if (bmi > 0) bmi / 25.0 else 0.0
|
||||
}
|
||||
|
||||
private fun calculatePonderalIndex(heightCm: Double, weightKg: Double): Double {
|
||||
if (heightCm <= 0 || weightKg <= 0) return 0.0
|
||||
val heightInMeters = heightCm / 100.0
|
||||
return weightKg / (heightInMeters * heightInMeters * heightInMeters)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 380)
|
||||
@Composable
|
||||
fun TipTimeLayoutPreview() {
|
||||
fun BmiCalculatorScreenPreview() {
|
||||
TipTimeTheme {
|
||||
TipTimeLayout()
|
||||
BmiCalculatorScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,78 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.example.tiptime.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val md_theme_light_primary = Color(0xFF984061)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFFFD9E2)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF3E001D)
|
||||
val md_theme_light_secondary = Color(0xFF754B9C)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFF1DBFF)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF2D0050)
|
||||
val md_theme_light_tertiary = Color(0xFF984060)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFFFD9E2)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF3E001D)
|
||||
// Custom Army Green & Brown Palette
|
||||
val armyGreen = Color(0xFF4B5320)
|
||||
val darkBrown = Color(0xFF5C4033)
|
||||
val lightCream = Color(0xFFF5F5DC)
|
||||
val tan = Color(0xFFD2B48C)
|
||||
|
||||
// Material 3 Light Theme Colors
|
||||
val md_theme_light_primary = armyGreen
|
||||
val md_theme_light_onPrimary = lightCream
|
||||
val md_theme_light_primaryContainer = tan
|
||||
val md_theme_light_onPrimaryContainer = darkBrown
|
||||
val md_theme_light_secondary = darkBrown
|
||||
val md_theme_light_onSecondary = lightCream
|
||||
val md_theme_light_secondaryContainer = tan
|
||||
val md_theme_light_onSecondaryContainer = darkBrown
|
||||
val md_theme_light_tertiary = tan
|
||||
val md_theme_light_onTertiary = darkBrown
|
||||
val md_theme_light_tertiaryContainer = armyGreen
|
||||
val md_theme_light_onTertiaryContainer = lightCream
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
val md_theme_light_background = Color(0xFFFAFCFF)
|
||||
val md_theme_light_onBackground = Color(0xFF001F2A)
|
||||
val md_theme_light_surface = Color(0xFFFAFCFF)
|
||||
val md_theme_light_onSurface = Color(0xFF001F2A)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFF2DDE2)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF514347)
|
||||
val md_theme_light_outline = Color(0xFF837377)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFE1F4FF)
|
||||
val md_theme_light_inverseSurface = Color(0xFF003547)
|
||||
val md_theme_light_inversePrimary = Color(0xFFFFB0C8)
|
||||
val md_theme_light_surfaceTint = Color(0xFF984061)
|
||||
val md_theme_light_outlineVariant = Color(0xFFD5C2C6)
|
||||
val md_theme_light_background = lightCream
|
||||
val md_theme_light_onBackground = darkBrown
|
||||
val md_theme_light_surface = lightCream
|
||||
val md_theme_light_onSurface = darkBrown
|
||||
val md_theme_light_surfaceVariant = tan
|
||||
val md_theme_light_onSurfaceVariant = darkBrown
|
||||
val md_theme_light_outline = armyGreen
|
||||
val md_theme_light_inverseOnSurface = lightCream
|
||||
val md_theme_light_inverseSurface = darkBrown
|
||||
val md_theme_light_inversePrimary = lightCream
|
||||
val md_theme_light_surfaceTint = armyGreen
|
||||
val md_theme_light_outlineVariant = tan
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFFFB0C8)
|
||||
val md_theme_dark_onPrimary = Color(0xFF5E1133)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF7B2949)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFFFD9E2)
|
||||
val md_theme_dark_secondary = Color(0xFFDEB7FF)
|
||||
val md_theme_dark_onSecondary = Color(0xFF44196A)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF5C3382)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFF1DBFF)
|
||||
val md_theme_dark_tertiary = Color(0xFFFFB1C7)
|
||||
val md_theme_dark_onTertiary = Color(0xFF5E1132)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF7B2948)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E2)
|
||||
// Material 3 Dark Theme Colors
|
||||
val md_theme_dark_primary = tan
|
||||
val md_theme_dark_onPrimary = darkBrown
|
||||
val md_theme_dark_primaryContainer = armyGreen
|
||||
val md_theme_dark_onPrimaryContainer = lightCream
|
||||
val md_theme_dark_secondary = tan
|
||||
val md_theme_dark_onSecondary = darkBrown
|
||||
val md_theme_dark_secondaryContainer = armyGreen
|
||||
val md_theme_dark_onSecondaryContainer = lightCream
|
||||
val md_theme_dark_tertiary = lightCream
|
||||
val md_theme_dark_onTertiary = armyGreen
|
||||
val md_theme_dark_tertiaryContainer = darkBrown
|
||||
val md_theme_dark_onTertiaryContainer = lightCream
|
||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
val md_theme_dark_onError = Color(0xFF690005)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_dark_background = Color(0xFF001F2A)
|
||||
val md_theme_dark_onBackground = Color(0xFFBFE9FF)
|
||||
val md_theme_dark_surface = Color(0xFF001F2A)
|
||||
val md_theme_dark_onSurface = Color(0xFFBFE9FF)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF514347)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFD5C2C6)
|
||||
val md_theme_dark_outline = Color(0xFF9E8C90)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF001F2A)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFBFE9FF)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF984061)
|
||||
val md_theme_dark_surfaceTint = Color(0xFFFFB0C8)
|
||||
val md_theme_dark_outlineVariant = Color(0xFF514347)
|
||||
val md_theme_dark_background = darkBrown
|
||||
val md_theme_dark_onBackground = lightCream
|
||||
val md_theme_dark_surface = darkBrown
|
||||
val md_theme_dark_onSurface = lightCream
|
||||
val md_theme_dark_surfaceVariant = armyGreen
|
||||
val md_theme_dark_onSurfaceVariant = lightCream
|
||||
val md_theme_dark_outline = tan
|
||||
val md_theme_dark_inverseOnSurface = darkBrown
|
||||
val md_theme_dark_inverseSurface = lightCream
|
||||
val md_theme_dark_inversePrimary = armyGreen
|
||||
val md_theme_dark_surfaceTint = tan
|
||||
val md_theme_dark_outlineVariant = armyGreen
|
||||
val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
@ -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>
|
||||
@ -1,24 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2023 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ https://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="app_name">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">BMI Calculator</string>
|
||||
<string name="height">Tinggi Badan (cm)</string>
|
||||
<string name="weight">Berat Badan (kg)</string>
|
||||
<string name="age">Umur</string>
|
||||
|
||||
<!-- Result Screen -->
|
||||
<string name="result_title">Result</string>
|
||||
<string name="bmi_value_category">BMI = %.1f kg/m² (%s)</string>
|
||||
<string name="healthy_bmi_range">Healthy BMI range: 18.5 kg/m² - 25 kg/m²</string>
|
||||
<string name="healthy_weight_for_height">Healthy weight for your height: %.1f kg - %.1f kg</string>
|
||||
<string name="bmi_prime">BMI Prime: %.2f</string>
|
||||
<string name="ponderal_index">Ponderal Index: %.1f kg/m³</string>
|
||||
|
||||
<!-- BMI Categories -->
|
||||
<string name="underweight">Underweight</string>
|
||||
<string name="normal">Normal</string>
|
||||
<string name="overweight">Overweight</string>
|
||||
<string name="obesity">Obesity</string>
|
||||
</resources>
|
||||
|
||||
@ -16,8 +16,8 @@
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id("com.android.application") version "8.8.0" apply false
|
||||
id("com.android.library") version "8.8.0" 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
|
||||
}
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user