mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-29 20:12:32 -04:00
Update unlock view UI (#1776)
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
package net.aliasvault.app.pinunlock
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -8,6 +11,7 @@ import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -167,6 +171,72 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
continueButton.setOnClickListener {
|
||||
submitPin()
|
||||
}
|
||||
|
||||
// Animate views in on appear
|
||||
animateViewsIn()
|
||||
}
|
||||
|
||||
private fun animateViewsIn() {
|
||||
// Fade in and translate title
|
||||
titleTextView.alpha = 0f
|
||||
titleTextView.translationY = -20f
|
||||
titleTextView.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(100)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
// Fade in subtitle
|
||||
subtitleTextView.alpha = 0f
|
||||
subtitleTextView.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(200)
|
||||
.start()
|
||||
|
||||
// Fade in and scale PIN display (dots or text)
|
||||
pinDotsContainer.alpha = 0f
|
||||
pinDotsContainer.scaleX = 0.95f
|
||||
pinDotsContainer.scaleY = 0.95f
|
||||
pinDotsContainer.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(300)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
pinTextView.alpha = 0f
|
||||
pinTextView.scaleX = 0.95f
|
||||
pinTextView.scaleY = 0.95f
|
||||
pinTextView.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(300)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
|
||||
// Fade in numpad buttons with staggered delay
|
||||
val numpadContainer = findViewById<View>(R.id.numpadContainer)
|
||||
numpadContainer.alpha = 0f
|
||||
numpadContainer.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(400)
|
||||
.start()
|
||||
|
||||
// Fade in continue button (if visible)
|
||||
continueButton.alpha = 0f
|
||||
continueButton.animate()
|
||||
.alpha(1f)
|
||||
.setDuration(400)
|
||||
.setStartDelay(500)
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun setupNumpad() {
|
||||
@@ -262,8 +332,19 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
val config = configuration ?: return
|
||||
if (continueButton.visibility == View.VISIBLE) {
|
||||
// Enable button only if PIN is at least 4 digits
|
||||
continueButton.isEnabled = currentPin.length >= 4
|
||||
continueButton.alpha = if (currentPin.length >= 4) 1.0f else 0.5f
|
||||
val isEnabled = currentPin.length >= 4
|
||||
continueButton.isEnabled = isEnabled
|
||||
|
||||
// Animate button scale and alpha based on enabled state
|
||||
val scale = if (isEnabled) 1.0f else 0.98f
|
||||
val alpha = if (isEnabled) 1.0f else 0.5f
|
||||
continueButton.animate()
|
||||
.scaleX(scale)
|
||||
.scaleY(scale)
|
||||
.alpha(alpha)
|
||||
.setDuration(200)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,8 +353,8 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
|
||||
val config = configuration ?: return
|
||||
|
||||
// Clear error when user starts typing
|
||||
errorTextView.visibility = View.GONE
|
||||
// Clear error when user starts typing with animation
|
||||
hideError()
|
||||
|
||||
// Check if we've reached max length
|
||||
val maxLength = config.pinLength ?: 8 // Max 8 digits in setup mode
|
||||
@@ -307,8 +388,8 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
// Remove last digit
|
||||
currentPin = currentPin.dropLast(1)
|
||||
|
||||
// Clear error
|
||||
errorTextView.visibility = View.GONE
|
||||
// Clear error with animation
|
||||
hideError()
|
||||
|
||||
// Update UI
|
||||
val config = configuration ?: return
|
||||
@@ -414,7 +495,7 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
delay(1000)
|
||||
configuration = viewModel.initializeConfiguration(PinMode.SETUP)
|
||||
currentPin = ""
|
||||
errorTextView.visibility = View.GONE
|
||||
hideError()
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
@@ -422,7 +503,33 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
|
||||
private fun showError(message: String) {
|
||||
errorTextView.text = message
|
||||
|
||||
// Animate error in with slide from top and fade
|
||||
errorTextView.visibility = View.VISIBLE
|
||||
errorTextView.alpha = 0f
|
||||
errorTextView.translationY = -20f
|
||||
errorTextView.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(300)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun hideError() {
|
||||
if (errorTextView.visibility == View.VISIBLE) {
|
||||
errorTextView.animate()
|
||||
.alpha(0f)
|
||||
.translationY(-20f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(AccelerateDecelerateInterpolator())
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
errorTextView.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun triggerErrorFeedback() {
|
||||
@@ -437,11 +544,17 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun shakeAndClear() {
|
||||
// Shake the PIN display to indicate error (similar to password field shake)
|
||||
val config = configuration ?: return
|
||||
val targetView = if (config.pinLength != null) pinDotsContainer else pinTextView
|
||||
val shake = ObjectAnimator.ofFloat(targetView, "translationX", 0f, 25f, -25f, 25f, -25f, 15f, -15f, 6f, -6f, 0f)
|
||||
shake.duration = 600
|
||||
shake.start()
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
// Clear the PIN after a short delay to show error
|
||||
delay(500)
|
||||
currentPin = ""
|
||||
val config = configuration ?: return@launch
|
||||
if (config.pinLength != null) {
|
||||
updatePinDots()
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Focused state: orange/primary color -->
|
||||
<item android:state_focused="true" android:color="@color/primary" />
|
||||
<!-- Hovered state: orange/primary color -->
|
||||
<item android:state_hovered="true" android:color="@color/primary" />
|
||||
<!-- Default state: subtle gray border visible in both light and dark mode -->
|
||||
<item android:color="#4D888888" />
|
||||
</selector>
|
||||
@@ -88,7 +88,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/password_unlock_password_hint"
|
||||
app:boxStrokeColor="@color/primary"
|
||||
app:boxStrokeColor="@color/password_field_stroke_color"
|
||||
app:boxStrokeWidth="2dp"
|
||||
app:hintTextColor="?android:attr/textColorSecondary"
|
||||
app:startIconDrawable="@drawable/ic_lock"
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<!-- Logo -->
|
||||
<ImageView
|
||||
android:id="@+id/logoImageView"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="70dp"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:contentDescription="@string/aliasvault_icon"
|
||||
android:elevation="4dp"
|
||||
@@ -37,13 +37,13 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pin_unlock_vault"
|
||||
android:textSize="22sp"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintTop_toBottomOf="@id/logoImageView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="12dp" />
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<!-- Subtitle -->
|
||||
<TextView
|
||||
@@ -51,13 +51,13 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pin_enter_to_unlock"
|
||||
android:textSize="15sp"
|
||||
android:textSize="16sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleTextView"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:paddingBottom="24dp" />
|
||||
|
||||
Reference in New Issue
Block a user