Add password visibility toggle

This commit is contained in:
Alex Gabor 2022-03-07 09:24:16 +02:00
parent e4ac3f78b6
commit b003e23305
5 changed files with 238 additions and 27 deletions

View file

@ -85,6 +85,7 @@ dependencies {
implementation "androidx.compose.ui:ui-tooling:$androidx_compose" implementation "androidx.compose.ui:ui-tooling:$androidx_compose"
implementation "androidx.compose.foundation:foundation:$androidx_compose" implementation "androidx.compose.foundation:foundation:$androidx_compose"
implementation "androidx.compose.material:material:$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-insets:$google_accompanist"
implementation "com.google.accompanist:accompanist-swiperefresh:$google_accompanist" implementation "com.google.accompanist:accompanist-swiperefresh:$google_accompanist"

View file

@ -17,6 +17,7 @@ class ComposeLoginRobot(
} }
fun assertPassword(password: String): ComposeLoginRobot = apply { fun assertPassword(password: String): ComposeLoginRobot = apply {
composeTestRule.onNodeWithTag(AuthScreenTag.PasswordVisibilityToggle).performClick()
composeTestRule.onNodeWithTag(AuthScreenTag.PasswordInput).assertTextContains(password) composeTestRule.onNodeWithTag(AuthScreenTag.PasswordInput).assertTextContains(password)
} }

View file

@ -1,12 +1,19 @@
package org.fnives.test.showcase.compose.screen.auth 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.layout.*
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier 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.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import com.google.accompanist.insets.statusBarsPadding import com.google.accompanist.insets.statusBarsPadding
@ -53,10 +61,8 @@ fun AuthScreen(
} }
} }
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
private fun CredentialsFields(authScreenState: AuthScreenState, modifier: Modifier = Modifier) { private fun CredentialsFields(authScreenState: AuthScreenState, modifier: Modifier = Modifier) {
val keyboardController = LocalSoftwareKeyboardController.current
Column( Column(
modifier modifier
.fillMaxWidth() .fillMaxWidth()
@ -64,33 +70,57 @@ private fun CredentialsFields(authScreenState: AuthScreenState, modifier: Modifi
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
OutlinedTextField( UsernameField(authScreenState)
value = authScreenState.username, PasswordField(authScreenState)
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)
)
} }
} }
@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 @Composable
private fun Snackbar(authScreenState: AuthScreenState, modifier: Modifier = Modifier) { private fun Snackbar(authScreenState: AuthScreenState, modifier: Modifier = Modifier) {
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
@ -114,7 +144,10 @@ private fun Snackbar(authScreenState: AuthScreenState, modifier: Modifier = Modi
@Composable @Composable
private fun LoginButton(modifier: Modifier = Modifier, onClick: () -> Unit) { private fun LoginButton(modifier: Modifier = Modifier, onClick: () -> Unit) {
Box(modifier) { Box(modifier) {
Button(onClick = onClick, Modifier.fillMaxWidth().testTag(AuthScreenTag.LoginButton)) { Button(onClick = onClick,
Modifier
.fillMaxWidth()
.testTag(AuthScreenTag.LoginButton)) {
Text(text = "Login") Text(text = "Login")
} }
} }
@ -140,4 +173,5 @@ object AuthScreenTag {
const val UsernameInput = "AuthScreenTag.UsernameInput" const val UsernameInput = "AuthScreenTag.UsernameInput"
const val PasswordInput = "AuthScreenTag.PasswordInput" const val PasswordInput = "AuthScreenTag.PasswordInput"
const val LoginButton = "AuthScreenTag.LoginButton" const val LoginButton = "AuthScreenTag.LoginButton"
const val PasswordVisibilityToggle = "AuthScreenTag.PasswordVisibilityToggle"
} }

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="NewApi">
<aapt:attr name="android:drawable">
<vector
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:name="strike_through"
android:pathData="@string/path_password_strike_through"
android:strokeColor="@android:color/white"
android:strokeLineCap="square"
android:strokeWidth="1.8"
android:trimPathEnd="0"/>
<group>
<clip-path
android:name="eye_mask"
android:pathData="@string/path_password_eye_mask_visible"/>
<path
android:fillColor="@android:color/white"
android:name="eye"
android:pathData="@string/path_password_eye"/>
</group>
</vector>
</aapt:attr>
<target android:name="eye_mask">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="@integer/hide_password_duration"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="pathData"
android:valueFrom="@string/path_password_eye_mask_visible"
android:valueTo="@string/path_password_eye_mask_strike_through"
android:valueType="pathType"/>
</aapt:attr>
</target>
<target android:name="strike_through">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="@integer/hide_password_duration"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="trimPathEnd"
android:valueFrom="0"
android:valueTo="1"/>
</aapt:attr>
</target>
</animated-vector>

View file

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="NewApi">
<aapt:attr name="android:drawable">
<vector
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:name="strike_through"
android:pathData="@string/path_password_strike_through"
android:strokeColor="@android:color/white"
android:strokeLineCap="square"
android:strokeWidth="1.8"/>
<group>
<clip-path
android:name="eye_mask"
android:pathData="@string/path_password_eye_mask_strike_through"/>
<path
android:fillColor="@android:color/white"
android:name="eye"
android:pathData="@string/path_password_eye"/>
</group>
</vector>
</aapt:attr>
<target android:name="eye_mask">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="@integer/show_password_duration"
android:interpolator="@android:interpolator/fast_out_linear_in"
android:propertyName="pathData"
android:valueFrom="@string/path_password_eye_mask_strike_through"
android:valueTo="@string/path_password_eye_mask_visible"
android:valueType="pathType"/>
</aapt:attr>
</target>
<target android:name="strike_through">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="@integer/show_password_duration"
android:interpolator="@android:interpolator/fast_out_linear_in"
android:propertyName="trimPathEnd"
android:valueFrom="1"
android:valueTo="0"/>
</aapt:attr>
</target>
</animated-vector>