Add password visibility toggle
This commit is contained in:
parent
e4ac3f78b6
commit
b003e23305
5 changed files with 238 additions and 27 deletions
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,31 +70,55 @@ 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) },
|
@OptIn(ExperimentalComposeUiApi::class, androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi::class)
|
||||||
modifier = Modifier.fillMaxWidth().testTag(AuthScreenTag.UsernameInput)
|
@Composable
|
||||||
)
|
private fun PasswordField(authScreenState: AuthScreenState) {
|
||||||
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = authScreenState.password,
|
value = authScreenState.password,
|
||||||
label = { Text(text = stringResource(id = R.string.password)) },
|
label = { Text(text = stringResource(id = R.string.password)) },
|
||||||
placeholder = { 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) },
|
onValueChange = { authScreenState.onPasswordChanged(it) },
|
||||||
keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Done, keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Done, keyboardType = KeyboardType.Password),
|
||||||
keyboardActions = KeyboardActions(onDone = {
|
keyboardActions = KeyboardActions(onDone = {
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
authScreenState.onLogin()
|
authScreenState.onLogin()
|
||||||
}),
|
}),
|
||||||
visualTransformation = PasswordVisualTransformation(),
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 16.dp)
|
.padding(top = 16.dp)
|
||||||
.testTag(AuthScreenTag.PasswordInput)
|
.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
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
88
app/src/main/res/drawable/avd_hide_password.xml
Normal file
88
app/src/main/res/drawable/avd_hide_password.xml
Normal 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>
|
||||||
87
app/src/main/res/drawable/avd_show_password.xml
Normal file
87
app/src/main/res/drawable/avd_show_password.xml
Normal 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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue