Tugas 8 PPB A 2025 - Water Bottle Material Design

Nama     : Rayhan Arvianta Bayuputra

NRP        : 5025211217

Kelas      : PPB A

TA            : 2024 (Genap)


Water Bottle App with Material Design

Pada pertemuan kali ini, kami membuat aplikasi sederhana botol air dengan material design. Berikut cuplikannya:

Berikut source codenya:

MainActivity.kt

package com.example.waterbottle

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.waterbottle.ui.theme.WaterBottleTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WaterBottleTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {

var usedWaterAmount by remember {
mutableStateOf(100)
}
val totalWaterAmount = remember {
2500
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {

WatterBottle(
totalWaterAmount = totalWaterAmount,
unit = "ml",
usedWaterAmount = usedWaterAmount
)
Spacer(modifier = Modifier.height(20.dp))
Text(
text = "Total Amount is : $totalWaterAmount",
textAlign = TextAlign.Center
)
Button(
onClick = { usedWaterAmount += 200 },
colors = ButtonDefaults.buttonColors(containerColor = Color(0xffff007f))
) {
Text(text = "Drink")
}
}


}
}
}
}
}

WaterBottle.kt

package com.example.waterbottle

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun WatterBottle(
modifier: Modifier = Modifier,
totalWaterAmount: Int,
unit: String,
usedWaterAmount: Int,
waterWavesColor: Color = Color(0xff279EFF),
bottleColor: Color = Color.White,
capColor: Color = Color(0xFF0065B9)
) {

val waterPercentage = animateFloatAsState(
targetValue = (usedWaterAmount.toFloat() / totalWaterAmount.toFloat()),
label = "Water Waves animation",
animationSpec = tween(durationMillis = 1000)
).value

val usedWaterAmountAnimation = animateIntAsState(
targetValue = usedWaterAmount,
label = "Used water amount animation",
animationSpec = tween(durationMillis = 1000)
).value

Box(
modifier = modifier
.width(200.dp)
.height(600.dp)
) {

Canvas(modifier = Modifier.fillMaxSize()) {
val width = size.width
val height = size.height

val capWidth = size.width * 0.55f
val capHeight = size.height * 0.13f

//Draw the bottle body
val bodyPath = Path().apply {
moveTo(width * 0.3f, height * 0.1f)
lineTo(width * 0.3f, height * 0.2f)
quadraticBezierTo(
0f, height * 0.3f, // The pulling point
0f, height * 0.4f
)
lineTo(0f, height * 0.95f)
quadraticBezierTo(
0f, height,
width * 0.05f, height
)

lineTo(width * 0.95f, height)
quadraticBezierTo(
width, height,
width, height * 0.95f
)
lineTo(width, height * 0.4f)
quadraticBezierTo(
width, height * 0.3f,
width * 0.7f, height * 0.2f
)
lineTo(width * 0.7f, height * 0.2f)
lineTo(width * 0.7f, height * 0.1f)

close()
}
clipPath(
path = bodyPath
) {
// Draw the color of the bottle
drawRect(
color = bottleColor,
size = size,
topLeft = Offset(0f, 0f)
)

//Draw the water waves
val waterWavesYPosition = (1 - waterPercentage) * size.height

val wavesPath = Path().apply {
moveTo(
x = 0f,
y = waterWavesYPosition
)
lineTo(
x = size.width,
y = waterWavesYPosition
)
lineTo(
x = size.width,
y = size.height
)
lineTo(
x = 0f,
y = size.height
)
close()
}
drawPath(
path = wavesPath,
color = waterWavesColor,
)
}

//Draw the bottle cap
drawRoundRect(
color = capColor,
size = Size(capWidth, capHeight),
topLeft = Offset(size.width / 2 - capWidth / 2f, 0f),
cornerRadius = CornerRadius(45f, 45f)
)


}
val text = buildAnnotatedString {
withStyle(
style = SpanStyle(
color = if (waterPercentage > 0.5f) bottleColor else waterWavesColor,
fontSize = 44.sp
)
) {
append(usedWaterAmountAnimation.toString())
}
withStyle(
style = SpanStyle(
color = if (waterPercentage > 0.5f) bottleColor else waterWavesColor,
fontSize = 22.sp
)
) {
append(" ")
append(unit)
}
}

Box(
modifier = Modifier
.fillMaxSize()
.fillMaxHeight(),
contentAlignment = Alignment.Center
) {
Text(text = text)
}
}
}


@Preview
@Composable
fun WaterBottlePreview() {
WatterBottle(
totalWaterAmount = 2500,
unit = "ml",
usedWaterAmount = 120
)
}

Theme.kt

package com.example.waterbottle.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 DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)

private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40

/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)

@Composable
fun WaterBottleTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
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
)
}

MainActivity Class

Kelas MainActivity adalah titik masuk utama dari aplikasi ini. Kelas ini merupakan turunan dari ComponentActivity. Di dalam metode onCreate(), antarmuka pengguna diinisialisasi menggunakan Jetpack Compose melalui fungsi setContent {}.

Seluruh tampilan dibungkus dalam WaterBottleTheme, dan dibalut oleh komponen Surface yang menggunakan MaterialTheme.colorScheme.background. Kemudian ditampilkan sebuah Column yang menampilkan:

  • Komponen visual WatterBottle(), yaitu botol air yang terisi.
  • Text: Menampilkan total kebutuhan air per hari (dalam mililiter).
  • Button: Tombol “Drink” yang menambahkan jumlah air yang sudah diminum sebanyak 200 ml setiap kali ditekan.

Variabel utama:

  • usedWaterAmount: Jumlah air yang telah diminum.
  • totalWaterAmount: Target jumlah air harian (misalnya 2500 ml).

WaterBottle Function

Fungsi WatterBottle() bertanggung jawab untuk menampilkan bentuk botol air yang terisi air secara animatif. Komponen ini dibangun menggunakan Canvas dan Path.

Parameter:

  • totalWaterAmount: Total target air minum (misal 2500 ml).
  • usedWaterAmount: Jumlah air yang telah dikonsumsi.
  • unit: Satuan (biasanya "ml").
  • waterWavesColor, bottleColor, capColor: Warna-warna kustom untuk air, botol, dan tutup.

Fitur Utama:

1. Animasi Air Terisi

  • Menggunakan animateFloatAsState dan animateIntAsState untuk mengisi botol secara halus.
  • Proporsi air berdasarkan rasio usedWaterAmount / totalWaterAmount.

2. Canvas & Path

  • Membentuk tubuh botol menggunakan Path dan quadraticBezierTo.
  • Air divisualisasikan dengan clipPath() agar hanya tampak di dalam botol.
  • Tutup botol digambar dengan drawRoundRect().

3. Jumlah Air dalam Botol

  • Teks jumlah air ditampilkan di tengah botol.
  • Warna teks disesuaikan agar kontras dengan warna air (jika > 50%, teks jadi putih).

WaterBottleTheme

Fungsi WaterBottleTheme() menerapkan sistem tema Material3, baik untuk dark mode maupun light mode. Juga mendukung Dynamic Color (Android 12+). Warna dasar yang digunakan diatur di DarkColorScheme dan LightColorScheme.

Button “Drink”

Tombol ini memungkinkan pengguna menambahkan 200 ml ke total konsumsi air setiap kali ditekan. Warna tombol menggunakan warna pink terang (#FF007F) untuk menonjolkan aksinya.


Demo Aplikasi


Comments

Popular posts from this blog

ETS PPB A 2025 - My Money Notes App

EAS PPB A 2025 - NoBoros Money App