This commit is contained in:
202310715066 NABILA SUWANDIRA 2025-11-07 13:36:53 +07:00
parent f4cbea5b6c
commit 502ae78685

View File

@ -0,0 +1,223 @@
package com.example.kalkulatorbmi
// 1. TAMBAHKAN IMPORT INI untuk mengakses resource seperti gambar
import com.example.kalkulatorbmi.R
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
// 2. Rapikan import yang duplikat
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun LoginScreen(navController: NavController) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
var isLoading by remember { mutableStateOf(false) }
// State untuk validasi error
var usernameError by remember { mutableStateOf<String?>(null) }
var passwordError by remember { mutableStateOf<String?>(null) }
// State untuk animasi
var isFormVisible by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
var shakeState by remember { mutableStateOf(false) }
// Animasi scale untuk efek "pop in" pada logo
val scale = animateFloatAsState(
targetValue = if (isFormVisible) 1f else 0.5f,
animationSpec = tween(durationMillis = 500), label = "logo_scale"
).value
LaunchedEffect(Unit) {
isFormVisible = true
}
val focusManager = LocalFocusManager.current
// 3. Latar Belakang Gradien
val gradientBrush = Brush.verticalGradient(
colors = listOf(
MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
MaterialTheme.colorScheme.background
)
)
Box(
modifier = Modifier
.fillMaxSize()
.background(gradientBrush),
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Animasi Logo dengan efek scale
Image(
// Kode ini sekarang akan berfungsi karena import R sudah ditambahkan
painter = painterResource(id = R.drawable.ic_launcher_foreground),
contentDescription = "Logo Aplikasi",
modifier = Modifier
.size(120.dp)
.scale(scale)
)
Text(text = "Selamat Datang", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(32.dp))
// 2. Animasi Goyang saat Error
val cardScale = animateFloatAsState(
targetValue = if (shakeState) 1.05f else 1f,
animationSpec = tween(100), label = "card_shake"
).value
AnimatedVisibility(
visible = isFormVisible,
enter = fadeIn(animationSpec = tween(1000, delayMillis = 500)) +
slideInVertically(
initialOffsetY = { it / 2 },
animationSpec = tween(1000, delayMillis = 500)
)
) {
Card(
shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
modifier = Modifier
.fillMaxWidth()
.scale(cardScale) // Terapkan animasi goyang
) {
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = username,
onValueChange = {
username = it
usernameError = null // Hapus error saat user mulai mengetik
},
label = { Text("Username") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
isError = usernameError != null, // Tampilkan error
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) })
)
// 1. Tampilkan pesan error di bawah field
if (usernameError != null) {
Text(
text = usernameError!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
Spacer(modifier = Modifier.height(if (usernameError != null) 8.dp else 16.dp))
OutlinedTextField(
value = password,
onValueChange = {
password = it
passwordError = null // Hapus error saat user mulai mengetik
},
label = { Text("Password") },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
isError = passwordError != null, // Tampilkan error
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
trailingIcon = {
val description = if (passwordVisible) "Sembunyikan password" else "Tampilkan password"
IconButton(onClick = { passwordVisible = !passwordVisible }) {
}
}
)
// Tampilkan pesan error di bawah field
if (passwordError != null) {
Text(
text = passwordError!!,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(start = 16.dp, top = 4.dp)
)
}
}
}
}
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
var hasError = false
if (username.isBlank()) {
usernameError = "Username tidak boleh kosong"
hasError = true
}
if (password.isBlank()) {
passwordError = "Password tidak boleh kosong"
hasError = true
}
if (hasError) {
// Jalankan animasi goyang
coroutineScope.launch {
shakeState = true
delay(100)
shakeState = false
}
} else {
isLoading = true
}
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary)
} else {
Text("Login")
}
}
if (isLoading) {
LaunchedEffect(Unit) {
delay(1500)
navController.navigate("main") {
popUpTo("login") { inclusive = true }
}
}
}
}
}
}