diff --git a/app/build.gradle b/app/build.gradle
index cb29971..4655d31 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -85,6 +85,7 @@ dependencies {
implementation "androidx.compose.ui:ui-tooling:$androidx_compose"
implementation "androidx.compose.foundation:foundation:$androidx_compose"
implementation "androidx.compose.material:material:$androidx_compose"
+ implementation "androidx.compose.animation:animation-graphics:$androidx_compose"
implementation "com.google.accompanist:accompanist-insets:$google_accompanist"
implementation "com.google.accompanist:accompanist-swiperefresh:$google_accompanist"
diff --git a/app/src/androidTest/java/org/fnives/test/showcase/ui/ComposeLoginRobot.kt b/app/src/androidTest/java/org/fnives/test/showcase/ui/ComposeLoginRobot.kt
index bfbf5a8..bb3db47 100644
--- a/app/src/androidTest/java/org/fnives/test/showcase/ui/ComposeLoginRobot.kt
+++ b/app/src/androidTest/java/org/fnives/test/showcase/ui/ComposeLoginRobot.kt
@@ -17,6 +17,7 @@ class ComposeLoginRobot(
}
fun assertPassword(password: String): ComposeLoginRobot = apply {
+ composeTestRule.onNodeWithTag(AuthScreenTag.PasswordVisibilityToggle).performClick()
composeTestRule.onNodeWithTag(AuthScreenTag.PasswordInput).assertTextContains(password)
}
diff --git a/app/src/main/java/org/fnives/test/showcase/compose/screen/auth/AuthScreen.kt b/app/src/main/java/org/fnives/test/showcase/compose/screen/auth/AuthScreen.kt
index 3cfeaae..8fc5cbb 100644
--- a/app/src/main/java/org/fnives/test/showcase/compose/screen/auth/AuthScreen.kt
+++ b/app/src/main/java/org/fnives/test/showcase/compose/screen/auth/AuthScreen.kt
@@ -1,12 +1,19 @@
package org.fnives.test.showcase.compose.screen.auth
+import androidx.compose.animation.graphics.res.animatedVectorResource
+import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
+import androidx.compose.animation.graphics.vector.AnimatedImageVector
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -16,6 +23,7 @@ import androidx.compose.ui.res.stringResource
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.constraintlayout.compose.ConstraintLayout
import com.google.accompanist.insets.statusBarsPadding
@@ -53,10 +61,8 @@ fun AuthScreen(
}
}
-@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CredentialsFields(authScreenState: AuthScreenState, modifier: Modifier = Modifier) {
- val keyboardController = LocalSoftwareKeyboardController.current
Column(
modifier
.fillMaxWidth()
@@ -64,33 +70,57 @@ private fun CredentialsFields(authScreenState: AuthScreenState, modifier: Modifi
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
- OutlinedTextField(
- value = authScreenState.username,
- label = { Text(text = stringResource(id = R.string.username)) },
- placeholder = { Text(text = stringResource(id = R.string.username)) },
- keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email, imeAction = ImeAction.Next),
- onValueChange = { authScreenState.onUsernameChanged(it) },
- modifier = Modifier.fillMaxWidth().testTag(AuthScreenTag.UsernameInput)
- )
- OutlinedTextField(
- value = authScreenState.password,
- label = { Text(text = stringResource(id = R.string.password)) },
- placeholder = { Text(text = stringResource(id = R.string.password)) },
- onValueChange = { authScreenState.onPasswordChanged(it) },
- keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Done, keyboardType = KeyboardType.Password),
- keyboardActions = KeyboardActions(onDone = {
- keyboardController?.hide()
- authScreenState.onLogin()
- }),
- visualTransformation = PasswordVisualTransformation(),
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp)
- .testTag(AuthScreenTag.PasswordInput)
- )
+ UsernameField(authScreenState)
+ PasswordField(authScreenState)
}
}
+@OptIn(ExperimentalComposeUiApi::class, androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi::class)
+@Composable
+private fun PasswordField(authScreenState: AuthScreenState) {
+ var passwordVisible by remember { mutableStateOf(false) }
+ val keyboardController = LocalSoftwareKeyboardController.current
+ OutlinedTextField(
+ value = authScreenState.password,
+ label = { Text(text = stringResource(id = R.string.password)) },
+ placeholder = { Text(text = stringResource(id = R.string.password)) },
+ trailingIcon = {
+ val image = AnimatedImageVector.animatedVectorResource(R.drawable.avd_show_password)
+ Icon(
+ painter = rememberAnimatedVectorPainter(image, passwordVisible),
+ contentDescription = null,
+ modifier = Modifier.clickable { passwordVisible = !passwordVisible }
+ .testTag(AuthScreenTag.PasswordVisibilityToggle)
+ )
+ },
+ onValueChange = { authScreenState.onPasswordChanged(it) },
+ keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Done, keyboardType = KeyboardType.Password),
+ keyboardActions = KeyboardActions(onDone = {
+ keyboardController?.hide()
+ authScreenState.onLogin()
+ }),
+ visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ .testTag(AuthScreenTag.PasswordInput)
+ )
+}
+
+@Composable
+private fun UsernameField(authScreenState: AuthScreenState) {
+ OutlinedTextField(
+ value = authScreenState.username,
+ label = { Text(text = stringResource(id = R.string.username)) },
+ placeholder = { Text(text = stringResource(id = R.string.username)) },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email, imeAction = ImeAction.Next),
+ onValueChange = { authScreenState.onUsernameChanged(it) },
+ modifier = Modifier
+ .fillMaxWidth()
+ .testTag(AuthScreenTag.UsernameInput)
+ )
+}
+
@Composable
private fun Snackbar(authScreenState: AuthScreenState, modifier: Modifier = Modifier) {
val snackbarState = remember { SnackbarHostState() }
@@ -114,7 +144,10 @@ private fun Snackbar(authScreenState: AuthScreenState, modifier: Modifier = Modi
@Composable
private fun LoginButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
Box(modifier) {
- Button(onClick = onClick, Modifier.fillMaxWidth().testTag(AuthScreenTag.LoginButton)) {
+ Button(onClick = onClick,
+ Modifier
+ .fillMaxWidth()
+ .testTag(AuthScreenTag.LoginButton)) {
Text(text = "Login")
}
}
@@ -140,4 +173,5 @@ object AuthScreenTag {
const val UsernameInput = "AuthScreenTag.UsernameInput"
const val PasswordInput = "AuthScreenTag.PasswordInput"
const val LoginButton = "AuthScreenTag.LoginButton"
+ const val PasswordVisibilityToggle = "AuthScreenTag.PasswordVisibilityToggle"
}
diff --git a/app/src/main/res/drawable/avd_hide_password.xml b/app/src/main/res/drawable/avd_hide_password.xml
new file mode 100644
index 0000000..89f4cdd
--- /dev/null
+++ b/app/src/main/res/drawable/avd_hide_password.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/avd_show_password.xml b/app/src/main/res/drawable/avd_show_password.xml
new file mode 100644
index 0000000..aed3983
--- /dev/null
+++ b/app/src/main/res/drawable/avd_show_password.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+