Buenas,
Tenia en mente des de hace tiempo de querer programar una pantalla de petición de código PIN (PassCode), similar a la pantalla de desbloqueo de muchos smarphones Android, para así añadir una capa de privacidad en apps que lo requieren.

Dividire los apuntes en 3 partes:
- Diseño de la pantalla de petición del código PIN
- Crear componente PassCodeDotsIndicator
- Parte lógica de entrada del PinCode, Comprobación del PinCode y animaciones
1 Diseño de la pantalla de petición del código PIN
Diseño del layout de la pantalla de petición de código pin, con ConstraintLayout ha sido más fácil de lo que me imaginaba
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".LockScreenActivity">
<ImageView
android:id="@+id/iconStateLock"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_lock_outline_24dp" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/auth_screen_enter_passcode"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iconStateLock" />
<com.webserveis.testlockscreen.IndicatorKeyDots
android:id="@+id/indicatorDots"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:indicatorType="FIXED"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTitle"
app:pinLength="4" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/keyPad"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/colorSurface"
android:padding="24dp"
android:theme="@style/KeyPad"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/indicatorDots">
<LinearLayout
android:id="@+id/btnDigit1"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@+id/btnDigit2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" "
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit2"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginStart="24dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toStartOf="@+id/btnDigit3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/btnDigit1"
app:layout_constraintTop_toTopOf="@+id/btnDigit1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="abc"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit3"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginStart="24dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/btnDigit2"
app:layout_constraintTop_toTopOf="@+id/btnDigit2">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="3"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="def"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit4"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@+id/btnDigit1"
app:layout_constraintTop_toBottomOf="@+id/btnDigit1">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="4"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ghi"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit5"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@+id/btnDigit2"
app:layout_constraintTop_toTopOf="@+id/btnDigit4">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="5"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="jkl"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit6"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@+id/btnDigit3"
app:layout_constraintTop_toTopOf="@+id/btnDigit5">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="6"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="mno"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit7"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="24dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@+id/btnDigit4"
app:layout_constraintTop_toBottomOf="@+id/btnDigit4">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="7"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="pqrs"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit8"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@+id/btnDigit5"
app:layout_constraintTop_toTopOf="@+id/btnDigit7">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="8"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="tuv"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit9"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@+id/btnDigit6"
app:layout_constraintTop_toTopOf="@+id/btnDigit8">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="9"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="wxyz"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/btnDigit0"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="@+id/btnDigit8"
app:layout_constraintTop_toBottomOf="@+id/btnDigit8">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<ImageView
android:id="@+id/btnDigitDel"
style="@style/KeyPad.DigitKey"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="center"
app:layout_constraintStart_toStartOf="@+id/btnDigit9"
app:layout_constraintTop_toTopOf="@+id/btnDigit0"
app:srcCompat="@drawable/ic_backspace_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Estilo propio para personalización res/values/styles.xml
<style name="KeyPad" parent="AppTheme" />
<style name="KeyPad.Light" parent="Theme.MaterialComponents.Light" />
<style name="KeyPad.Dark" parent="Theme.MaterialComponents" />
<style name="KeyPad.DigitKey">
<item name="android:background">?attr/selectableItemBackgroundBorderless</item>
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
</style>
2 Crear componente PassCodeIndicator
Ver el apunte Crear componente PassCodeDotsIndicator
3 Parte lógica
Teniendo la parte visual realizada, ahora falta darle la funcionalidad, detectar las pulsaciones de los botones, ir llenando el passCode y su validación
Para hacerlo más fácil crear un Manager PinLockManager.kt
class PinLokManager {
var pinCode: String = ""
var passCode: String = "1234"
var listener: PinLockListener? = null
fun appendDigit(c: Char) {
if (pinCode.isEmpty()) listener?.onPinLockStarted()
if (pinCode.length in 0..passCode.length) {
pinCode += c
listener?.onPinLockProgress(pinCode.length)
}
if (pinCode.length == passCode.length)
listener?.onPinLockValidated(pinCode == passCode)
}
fun removeLastDigit() {
pinCode = pinCode.dropLast(1)
listener?.onPinLockProgress(pinCode.length)
if (pinCode.isEmpty()) {
listener?.onPinLockCleared()
}
}
fun clearPinLock() {
pinCode = ""
listener?.onPinLockCleared()
}
fun passCodeSize(): Int {
return passCode.length
}
interface PinLockListener {
fun onPinLockStarted()
fun onPinLockProgress(value: Int)
fun onPinLockValidated(isValid: Boolean)
fun onPinLockCleared()
}
}
Crear el LockScreenActivity.kt
class LockScreenActivity : AppCompatActivity() {
private val uiScope = CoroutineScope(Dispatchers.Main + Job())
private val mPinLockManager: PinLokManager = PinLokManager()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_lock_screen)
mPinLockManager.passCode = "1234"
mPinLockManager.listener = pinLockListener()
indicatorDots.setPinLength(mPinLockManager.passCodeSize())
btnDigit0.setOnClickListener {
mPinLockManager.appendDigit('0')
}
btnDigit1.setOnClickListener {
mPinLockManager.appendDigit('1')
}
btnDigit2.setOnClickListener {
mPinLockManager.appendDigit('2')
}
btnDigit3.setOnClickListener {
mPinLockManager.appendDigit('3')
}
btnDigit4.setOnClickListener {
mPinLockManager.appendDigit('4')
}
btnDigit5.setOnClickListener {
mPinLockManager.appendDigit('5')
}
btnDigit6.setOnClickListener {
mPinLockManager.appendDigit('6')
}
btnDigit7.setOnClickListener {
mPinLockManager.appendDigit('7')
}
btnDigit8.setOnClickListener {
mPinLockManager.appendDigit('8')
}
btnDigit9.setOnClickListener {
mPinLockManager.appendDigit('9')
}
btnDigitDel.setOnClickListener {
mPinLockManager.removeLastDigit()
}
}
private fun pinLockListener(): PinLokManager.PinLockListener? = object : PinLokManager.PinLockListener {
override fun onPinLockStarted() {}
override fun onPinLockProgress(value: Int) {
indicatorDots.updateDot(value)
}
override fun onPinLockValidated(isValid: Boolean) {
uiScope.launch {
if (isValid) {
iconStateLock.setImageResource(R.drawable.ic_lock_open_24dp)
delay(500L)
setResult(Activity.RESULT_OK)
finish()
} else {
shake()
delay(500L)
mPinLockManager.clearPinLock()
}
}
}
override fun onPinLockCleared() {
indicatorDots.setPinLength(mPinLockManager.passCodeSize())
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
Log.d("TAG", "onKeyDown() called with: keyCode = [$keyCode], event = [$event]")
if (event != null && keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_DOWN) {
if (mPinLockManager.pinCode.isNotEmpty()) {
btnDigitDel.performClick()
} else {
setResult(Activity.RESULT_CANCELED)
finish()
}
return true
} else {
when (keyCode) {
in 7..16 -> {
mPinLockManager.appendDigit((keyCode - 7).toChar())
}
KeyEvent.KEYCODE_DEL -> mPinLockManager.removeLastDigit()
}
return super.onKeyDown(keyCode, event)
}
}
private fun shake() {
val objectAnimator = ObjectAnimator.ofFloat(
keyPad,
View.TRANSLATION_X,
0F, 15F, -15F, 15F, -15F, 6F, -6F, 3F, -3F, 0F
).setDuration(750)
objectAnimator.start()
}
}