login
This commit is contained in:
parent
f4cbea5b6c
commit
502ae78685
223
app/src/main/java/com/example/kalkulatorbmi/LoginScreen.kt
Normal file
223
app/src/main/java/com/example/kalkulatorbmi/LoginScreen.kt
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user