Pertemuan 10 - Water Bottle

Naufal Khairul Rizky - 5025221127


Bottle Water UI - Pertemuan 10

Aplikasi ini merupakan implementasi tampilan visual pengisian air dalam botol menggunakan Jetpack Compose. Aplikasi ini menampilkan indikator botol air yang bisa diisi secara bertahap saat tombol ditekan, sebagai simulasi proses pengisian air secara interaktif.


1. MainActivity.kt

Saat aplikasi dijalankan, fungsi onCreate() akan dipanggil:

  • enableEdgeToEdge() digunakan agar tampilan aplikasi memenuhi seluruh layar perangkat tanpa batasan edge.

  • setContent { ... } memanggil fungsi BottleWaterApp() sebagai UI utama yang akan ditampilkan.

  • MaterialTheme digunakan untuk menerapkan gaya desain Material 3 pada seluruh antarmuka aplikasi.


2. BottleWaterApp()

Fungsi @Composable utama yang membangun antarmuka pengguna secara keseluruhan.

a. State Management

Menggunakan remember dan mutableStateOf untuk mengatur status:

  • usedWaterAmount: Menyimpan jumlah air yang sudah terisi di dalam botol. Nilai ini akan meningkat setiap kali tombol ditekan.

  • totalWaterAmount: Menentukan kapasitas maksimum air dalam botol (misalnya 1000 ml).

  • incrementStep: Besarnya peningkatan air setiap kali tombol ditekan.

  • animationDelay: Waktu jeda antar peningkatan untuk membuat efek animasi halus saat mengisi.

  • coroutineScope: Digunakan untuk menjalankan coroutine saat tombol ditekan, dengan rememberCoroutineScope().


b. Struktur Tata Letak UI

Menggunakan Column untuk menyusun komponen secara vertikal dan rata tengah.

Komponen-Komponen:
  • Progress Bar
    Menggunakan Box dan Canvas untuk menggambarkan botol air:

    • Tinggi dari air yang ditampilkan dihitung berdasarkan persentase dari usedWaterAmount terhadap totalWaterAmount.

    • Tampilan air di dalam botol divisualisasikan sebagai Color.Cyan yang tumbuh dari bawah ke atas.

    • Label di atas botol menampilkan jumlah air saat ini dalam satuan ml.

  • Tombol "Fill"
    Menggunakan Button untuk mengisi botol:

    • Saat ditekan, coroutine akan dijalankan melalui scope.launch { ... } untuk menambah jumlah air secara bertahap dengan delay.

    • Efek animasi dicapai dengan menambah usedWaterAmount dalam step kecil (misalnya 20 ml) dengan jeda waktu tertentu (animationDelay).


3. Logika Animasi Pengisian

  • launch digunakan untuk menjalankan coroutine di dalam rememberCoroutineScope.

  • delay() digunakan untuk menunda setiap langkah pengisian air agar terlihat seperti animasi bertahap.

  • coerceAtMost(totalWaterAmount) memastikan nilai tidak melebihi batas maksimum kapasitas botol.



Repositori: Github

Source Code:
MainActivity.kt:
package com.example.waterbottle

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
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.unit.dp
import com.example.waterbottle.ui.theme.WaterBottleTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import androidx.compose.runtime.rememberCoroutineScope


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WaterBottleTheme {
MainScreen()
}
}
}
}

@Composable
fun MainScreen() {
var usedWaterAmount by remember { mutableStateOf(0) }
val totalWaterAmount = 2400
val scope = rememberCoroutineScope()

val incrementStep = 200
val animationDelay = 100L

Column(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF111111)),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
WaterBottle(
totalWaterAmount = totalWaterAmount,
unit = "ml",
usedWaterAmount = usedWaterAmount
)

Spacer(modifier = Modifier.height(24.dp))

Text(
text = "Total amount is: $totalWaterAmount",
color = Color.White
)

Spacer(modifier = Modifier.height(8.dp))

Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
// Button Fill (gradual fill)
Button(
onClick = {
if (usedWaterAmount < totalWaterAmount) {
scope.launch {
val target = usedWaterAmount + incrementStep
for (i in usedWaterAmount + 20..target step 20) {
usedWaterAmount = i.coerceAtMost(totalWaterAmount)
delay(animationDelay)
}
}
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4CAF50))
) {
Text("Fill", color = Color.White)
}

// Button Drink (gradual decrease)
Button(
onClick = {
if (usedWaterAmount > 0) {
scope.launch {
val target = usedWaterAmount - incrementStep
for (i in usedWaterAmount - 20 downTo target step 20) {
usedWaterAmount = i.coerceAtLeast(0)
delay(animationDelay)
}
}
}
},
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6C4AB6))
) {
Text("Drink", color = Color.White)
}

// Button Drink All (instant reset)
Button(
onClick = { usedWaterAmount = 0 },
colors = ButtonDefaults.buttonColors(containerColor = Color.Red)
) {
Text("Drink All", color = Color.White)
}
}
}
}

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.background
import androidx.compose.foundation.layout.Box
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.runtime.getValue
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.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun WaterBottle(
modifier: Modifier = Modifier,
totalWaterAmount: Int,
unit: String,
usedWaterAmount: Int,
waterColor: Color = Color(0xff279eff),
bottleColor: Color = Color.White,
capColor: Color = Color(0xff0065b9)
) {
val waterPercentage by animateFloatAsState(
targetValue = usedWaterAmount.toFloat() / totalWaterAmount.toFloat(),
label = "Water Waves Animation",
animationSpec = tween(durationMillis = 1000)
)

val usedWaterAmountAnimation by animateIntAsState(
targetValue = usedWaterAmount,
label = "Used Water Amount Animation",
animationSpec = tween(durationMillis = 1000)
)

Box(
modifier = modifier
.width(200.dp)
.height(600.dp)
.background(Color(0xFF111111)) // Full dark background
) {
Canvas(modifier = Modifier.fillMaxSize()) {
val width = size.width
val height = size.height
val capWidth = size.width * 0.55f
val capHeight = size.height * 0.13f

// Bottle shape
val bottleBodyPath = Path().apply {
moveTo(x = width * 0.3f, y = height * 0.1f)
lineTo(x = width * 0.3f, y = height * 0.2f)
quadraticBezierTo(x1 = 0f, y1 = height * 0.3f, x2 = 0f, y2 = height * 0.4f)
lineTo(x = 0f, y = height * 0.95f)
quadraticBezierTo(x1 = 0f, y1 = height, x2 = width * 0.05f, y2 = height)
lineTo(x = width * 0.95f, y = height)
quadraticBezierTo(x1 = width, y1 = height, x2 = width, y2 = height * 0.95f)
lineTo(x = width, y = height * 0.4f)
quadraticBezierTo(x1 = width, y1 = height * 0.3f, x2 = width * 0.7f, y2 = height * 0.2f)
lineTo(x = width * 0.7f, y = height * 0.1f)
close()
}

// Draw bottle fill
clipPath(bottleBodyPath) {
drawRect(
color = bottleColor,
size = size,
)
val waterY = (1 - waterPercentage) * size.height
val waterPath = Path().apply {
moveTo(x = 0f, y = waterY)
lineTo(x = size.width, y = waterY)
lineTo(x = size.width, y = size.height)
lineTo(x = 0f, y = size.height)
close()
}
drawPath(
path = waterPath,
color = waterColor
)
}

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

// Removed bottle outline
}

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

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

Komentar