This commit is contained in:
202310715096 JEREMIA SEBASTIAN MARPAUNG 2025-11-07 13:24:45 +07:00
parent b029b3dd10
commit f17e580397
8 changed files with 508 additions and 383 deletions

View File

@ -1,24 +1,59 @@
Tip Time - Solution Code # Aplikasi Kalkulator BMI
=================================
Solution code for the [Android Basics with Compose](https://developer.android.com/courses/android-basics-compose/course): Tip Time app. **Dibuat oleh:**
**Nama:** Jeremia Sebastian Marpaung
**NPM:** 202310715096
**Kelas:** Pemrograman Perangkat Bergerak - F5A5
---
## Deskripsi Aplikasi
Aplikasi **Kalkulator BMI (Body Mass Index)** ini dibuat sebagai proyek akhir mata kuliah **Pemrograman Perangkat Bergerak**.
Tujuannya adalah membantu pengguna menghitung **Indeks Massa Tubuh (BMI)** berdasarkan **berat badan (kg/lbs)** dan **tinggi badan (cm/inci)** agar dapat mengetahui apakah berat badan tergolong **Kurus(Underweight),Normal,Overweight,Obesitas**.
Aplikasi memiliki **satu halaman utama**:
1.**Halaman Utama (Kalkulator BMI)**
Pengguna dapat menginput berat dan tinggi badan, menekan tombol **“Hitung BMI”**, dan melihat hasil nilai BMI beserta **kategori serta saran kesehatannya**.
---
## Fitur Utama
- Input berat dan tinggi badan secara interaktif (bisa satuan **SI** atau **USC**).
- Perhitungan otomatis nilai BMI dengan opsi pembulatan hasil.
- Tampilan kategori hasil (Kurus(Underweight), Normal, Overweight, Obesitas).
- Antarmuka sederhana, bersih, dan responsif menggunakan **Jetpack Compose**.
- Navigasi antarhalaman dengan tombol **MULAI** dari halaman biodata.
---
## Teknologi yang Digunakan
- **Android Studio (Kotlin)**
- **Jetpack Compose** & **XML Layouts** untuk desain antarmuka
- **Intent** untuk navigasi antar activity
- **Drawable XML & colors.xml** untuk tema warna dan efek gradasi
- **Unit Test (disarankan)** untuk menguji akurasi perhitungan BMI
---
## Kontribusi & Kredit
Aplikasi ini dikembangkan dengan bantuan **Claude.ai** dalam pembuatan kode, desain antarmuka, dan dokumentasi.
Semua logika perhitungan, pengujian, dan penyempurnaan dilakukan mandiri oleh pengembang.
---
## Change Log (Ringkas)
- **Migrasi kode dasar** dari kalkulator tip ke kalkulator BMI berbasis Kotlin Compose.
- **Penambahan mode satuan USC (Inci & Lbs)** dengan validasi tinggi minimal 4 inci.
- **Perbaikan rumus perhitungan BMI** agar sesuai standar WHO.
- **Desain ulang Splash Screen** dengan tombol “MULAI” berwarna hijau dan latar gradasi biru-hijau.
- **Optimalisasi UX** — hasil BMI hanya muncul setelah tombol **“Hitung BMI”** ditekan.
Introduction ---
------------
The Tip Time app contains various UI elements for calculating a tip,
teaching about user input, and State in Compose.
## Lisensi
Proyek ini dibuat untuk tujuan pembelajaran dalam mata kuliah **Pemrograman Perangkat Bergerak** dan tidak untuk tujuan komersial.
Lisensi mengikuti [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
Pre-requisites ---
--------------
* Experience with Kotlin syntax.
* How to create and run a project in Android Studio.
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.

View File

@ -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,6 +5,7 @@ plugins {
} }
android { android {
namespace = "com.example.tiptime"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
@ -45,41 +30,46 @@ android {
) )
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString() jvmTarget = "17"
} }
buildFeatures { buildFeatures {
compose = true compose = true
} }
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
namespace = "com.example.tiptime"
} }
dependencies { dependencies {
// Compose BOM
implementation(platform("androidx.compose:compose-bom:2024.02.00"))
implementation(platform("androidx.compose:compose-bom:2024.12.01")) // Jetpack Compose
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling") implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.core:core-ktx:1.15.0") debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7") implementation("androidx.compose.material:material-icons-extended")
// AndroidX
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")
implementation("androidx.navigation:navigation-compose:2.7.7")
// Testing
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("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
debugImplementation("androidx.compose.ui:ui-test-manifest")
} }

View File

@ -1,20 +1,4 @@
<?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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
@ -24,12 +8,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TipTime" android:theme="@android:style/Theme.Material.Light.NoActionBar"
tools:targetApi="33"> tools:targetApi="33">
<activity <activity
android:name=".MainActivity" android:name="com.example.basic_android_kotlin_compose_training_tip_calculator.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.TipTime"> android:theme="@android:style/Theme.Material.Light.NoActionBar">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

View File

@ -0,0 +1,3 @@
package com.example
annotation class BMIcalculatorScreen

View File

@ -0,0 +1,422 @@
package com.example.basic_android_kotlin_compose_training_tip_calculator
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlin.math.round
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BMICalculatorApp()
}
}
}
@Composable
fun BMICalculatorApp() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") { HomePage(navController) }
composable("calculator") { CalculatorPage(navController) }
composable("info") { InfoPage(navController) }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomePage(navController: NavHostController) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("BMI Calculator") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF00BCD4),
titleContentColor = Color.White
)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
"Selamat Datang!",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF6200EE)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Hitung BMI Anda dengan mudah",
fontSize = 18.sp,
color = Color.Gray
)
Spacer(modifier = Modifier.height(48.dp))
Button(
onClick = { navController.navigate("calculator") },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF6200EE)
)
) {
Text("Mulai Hitung BMI", fontSize = 18.sp)
}
Spacer(modifier = Modifier.height(16.dp))
OutlinedButton(
onClick = { navController.navigate("info") },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(12.dp)
) {
Icon(Icons.Default.Info, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Informasi BMI", fontSize = 18.sp)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CalculatorPage(navController: NavHostController) {
var unitSystem by remember { mutableStateOf("Metric") }
var weight by remember { mutableStateOf("") }
var height by remember { mutableStateOf("") }
var feet by remember { mutableStateOf("") }
var inches by remember { mutableStateOf("") }
var bmi by remember { mutableDoubleStateOf(0.0) }
var category by remember { mutableStateOf("") }
var color by remember { mutableStateOf(Color.Gray) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Kalkulator BMI") },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back", tint = Color.White)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF6200EE),
titleContentColor = Color.White
)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Unit System Selector
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
FilterChip(
selected = unitSystem == "Metric",
onClick = {
unitSystem = "Metric"
weight = ""
height = ""
bmi = 0.0
},
label = { Text("Metric (kg/cm)") },
modifier = Modifier.weight(1f).padding(end = 8.dp)
)
FilterChip(
selected = unitSystem == "USC",
onClick = {
unitSystem = "USC"
weight = ""
feet = ""
inches = ""
bmi = 0.0
},
label = { Text("USC (lbs/ft/in)") },
modifier = Modifier.weight(1f).padding(start = 8.dp)
)
}
Spacer(modifier = Modifier.height(24.dp))
if (unitSystem == "Metric") {
// Metric System
OutlinedTextField(
value = weight,
onValueChange = { weight = it },
label = { Text("Berat Badan (kg)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = height,
onValueChange = { height = it },
label = { Text("Tinggi Badan (cm)") },
modifier = Modifier.fillMaxWidth()
)
} else {
// USC System
OutlinedTextField(
value = weight,
onValueChange = { weight = it },
label = { Text("Berat Badan (lbs)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedTextField(
value = feet,
onValueChange = { feet = it },
label = { Text("Tinggi (feet)") },
modifier = Modifier.weight(1f)
)
OutlinedTextField(
value = inches,
onValueChange = { inches = it },
label = { Text("Tinggi (inch)") },
modifier = Modifier.weight(1f)
)
}
}
Spacer(modifier = Modifier.height(20.dp))
Button(
onClick = {
if (unitSystem == "Metric") {
val w = weight.toDoubleOrNull() ?: 0.0
val h = (height.toDoubleOrNull() ?: 0.0) / 100.0
bmi = if (h > 0) round(w / (h * h) * 100) / 100.0 else 0.0
} else {
val w = weight.toDoubleOrNull() ?: 0.0
val f = feet.toDoubleOrNull() ?: 0.0
val i = inches.toDoubleOrNull() ?: 0.0
val totalInches = (f * 12) + i
// BMI = (weight in lbs / (height in inches)^2) * 703
bmi = if (totalInches > 0) round((w / (totalInches * totalInches)) * 703 * 100) / 100.0 else 0.0
}
when {
bmi < 18.5 -> {
category = "Kurus (Underweight)"
color = Color(0xFF42A5F5)
}
bmi < 25.0 -> {
category = "Normal"
color = Color(0xFF66BB6A)
}
bmi < 30.0 -> {
category = "Overweight"
color = Color(0xFFFFA726)
}
else -> {
category = "Obesitas"
color = Color(0xFFEF5350)
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(12.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF6200EE)
)
) {
Text("Hitung BMI", fontSize = 18.sp)
}
Spacer(modifier = Modifier.height(24.dp))
if (bmi > 0.0) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = color.copy(alpha = 0.2f)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "BMI: $bmi",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = color
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = category,
fontSize = 22.sp,
fontWeight = FontWeight.Medium,
color = color
)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InfoPage(navController: NavHostController) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Informasi BMI") },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back", tint = Color.White)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF6200EE),
titleContentColor = Color.White
)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(24.dp)
) {
Text(
"Kategori BMI",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
color = Color(0xFF6200EE)
)
Spacer(modifier = Modifier.height(16.dp))
BMIInfoCard("Kurus (Underweight)", "BMI < 18.5", Color(0xFF42A5F5))
Spacer(modifier = Modifier.height(12.dp))
BMIInfoCard("Normal", "BMI 18.5 - 24.9", Color(0xFF66BB6A))
Spacer(modifier = Modifier.height(12.dp))
BMIInfoCard("Overweight", "BMI 25.0 - 29.9", Color(0xFFFFA726))
Spacer(modifier = Modifier.height(12.dp))
BMIInfoCard("Obesitas", "BMI ≥ 30.0", Color(0xFFEF5350))
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color(0xFFF5F5F5)
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
"Tentang BMI",
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Body Mass Index (BMI) adalah ukuran lemak tubuh berdasarkan tinggi dan berat badan. BMI adalah indikator yang baik untuk mengetahui apakah berat badan Anda ideal atau tidak.",
fontSize = 14.sp,
lineHeight = 20.sp
)
}
}
}
}
}
@Composable
fun BMIInfoCard(title: String, range: String, color: Color) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = color.copy(alpha = 0.2f)
),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = title,
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
color = color
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = range,
fontSize = 14.sp,
color = color.copy(alpha = 0.8f)
)
}
}
}
}

View File

@ -1,196 +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.
*/
package com.example.tiptime
import android.os.Bundle
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.foundation.rememberScrollState
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
TipTimeTheme {
Surface(
modifier = Modifier.fillMaxSize(),
) {
TipTimeLayout()
}
}
}
}
}
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
var roundUp by remember { mutableStateOf(false) }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount, tipPercent, roundUp)
Column(
modifier = Modifier
.statusBarsPadding()
.padding(horizontal = 40.dp)
.verticalScroll(rememberScrollState())
.safeDrawingPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
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))
}
}
@Composable
fun EditNumberField(
@StringRes label: Int,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
onValueChanged: (String) -> Unit,
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
)
}
@Composable
fun RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.round_up_tip))
Switch(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
checked = roundUp,
onCheckedChange = onRoundUpChanged
)
}
}
/**
* 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)
}
@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
TipTimeTheme {
TipTimeLayout()
}
}

View File

@ -1,115 +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.
*/
package com.example.tiptime.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val LightColorScheme = lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
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
fun TipTimeTheme(
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
) {
val colorScheme = when {
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(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -20,5 +20,7 @@
<string name="bill_amount">Bill Amount</string> <string name="bill_amount">Bill Amount</string>
<string name="how_was_the_service">Tip Percentage</string> <string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string> <string name="round_up_tip">Round up tip?</string>
<string name="tip_amount">Tip Amount: %s</string> <string name="tip_amount">Tip Amount: %1$s</string>
</resources> </resources>