自定义数字解锁功能,如下所示:
>>>>>>>>>>>>
#### 接着上篇的 数字解锁键盘(LockKeypad)
1.仔细观察星号,这里密码显示的星号(*)是设计师提供设计icon。当然,如果有不需要的可以把对应代码注掉就行;
星号(icon)替换代码:(这里的代码在自定义view里都有,我只是单独贴出来)
input.addTextChangedListener {
if (it.isNullOrEmpty()) {
clear_text.hide()
button_delete.invisible()
button_enter.isEnabled = false
password_display.text = ""
} else {
clear_text.show()
button_delete.show()
button_enter.isEnabled = true
// Add password_display TextView: used here to display asterisks
password_display.text = it.toDisplayByInput(context)
}
}
// 循环替换每个数字
fun Editable.toDisplayByInput(context: Context): CharSequence {
val length = this.length
if(length == 0) return ""
val sb = SpannableStringBuilder()
for (i in 0 until length) {
sb.append(displayWithIcon(context))
}
return sb
}
// 用星号(Icon)来替换每个数字
private fun displayWithIcon(context: Context): Spannable {
val sb = SpannableStringBuilder()
sb.append(" Icon ")
val index = sb.indexOf("Icon")
sb.setSpan(
ImageSpan(context, R.drawable.ic_password_hidden_display, ImageSpan.ALIGN_CENTER),
index,
index + "Icon".length,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
return sb
}
1.CustomView >>> LockKeypad:(代码实现)
class LockKeypad @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = -1
) : ConstraintLayout(context, attrs, defStyleAttr) {
var onSuccessListener: ((String) -> Unit)? = null
//Shaking margin value in the specified time
private val shakeTime: Array<Float> by lazy {
arrayOf(0F, 0.2F, 0.4F, 0.6F, 0.8F, 1.0F)
}
private val shakeMargin: Array<Float> by lazy {
arrayOf(30F, -30F, 15F, -15F, 8F, 0F)
}
init {
context.getLayoutInflater().inflate(R.layout.view_lock_keypad, this, true)
password_display.show()
input.invisible()
button_one.setOnClickListener { appendNumber("1") }
button_two.setOnClickListener { appendNumber("2") }
button_three.setOnClickListener { appendNumber("3") }
button_four.setOnClickListener { appendNumber("4") }
button_five.setOnClickListener { appendNumber("5") }
button_six.setOnClickListener { appendNumber("6") }
button_seven.setOnClickListener { appendNumber("7") }
button_eight.setOnClickListener { appendNumber("8") }
button_nine.setOnClickListener { appendNumber("9") }
button_zero.setOnClickListener { appendNumber("0") }
var isLongClick = false
val runnable = object : Runnable {
override fun run() {
isLongClick = true
input.removeLast()
handler.postDelayed(
this,
50L
)
}
}
button_delete.setOnTouchListener { _, event ->
hideError()
if (visibility == View.INVISIBLE) {
false
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
handler.postDelayed(
runnable,
android.view.ViewConfiguration.getLongPressTimeout().toLong()
)
}
MotionEvent.ACTION_UP -> {
handler.removeCallbacks(runnable)
if (!isLongClick) {
input.removeLast()
}
isLongClick = false
}
}
true
}
button_enter.setOnClickListener {
onSuccessListener?.let { it1 -> it1(input.text.toString()) }
}
clear_text.setOnClickListener {
input.text.clear()
hideError()
}
input.addTextChangedListener {
if (it.isNullOrEmpty()) {
clear_text.hide()
button_delete.invisible()
button_enter.isEnabled = false
password_display.text = ""
} else {
clear_text.show()
button_delete.show()
button_enter.isEnabled = true
// Add password_display TextView: used here to display asterisks
password_display.text = it.toDisplayByInput(context)
}
}
}
fun showError() {
error_label.show()
shakeKeyframe(password_display, 400L).start()
}
fun hideError() {
error_label.hide()
}
private fun appendNumber(num: String) {
hideError()
input.text.append(num)
input.playSoundEffect(SoundEffectConstants.CLICK) // 为此视图播放声音效果;该框架将为某些内置动作(例如单击)播放声音效果
}
/**
* Shake left and right within a specified time
* @param view
* @param duration
* @return
*/
private fun shakeKeyframe(view: View, duration: Long): ObjectAnimator {
val pvhTranslateX = PropertyValuesHolder.ofKeyframe(
View.TRANSLATION_X,
Keyframe.ofFloat(shakeTime[0], shakeMargin[0]),
Keyframe.ofFloat(shakeTime[1], shakeMargin[1]),
Keyframe.ofFloat(shakeTime[2], shakeMargin[2]),
Keyframe.ofFloat(shakeTime[3], shakeMargin[3]),
Keyframe.ofFloat(shakeTime[4], shakeMargin[4]),
Keyframe.ofFloat(shakeTime[5], shakeMargin[5])
)
return ObjectAnimator.ofPropertyValuesHolder(view, pvhTranslateX).setDuration(duration)
}
}
Layout:view_lock_keypad(自定义view对应的布局文件)
<?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"
android:clipToPadding="false"
tools:ignore="HardcodedText">
<TextView
android:id="@+id/error_label"
style="@style/errorRed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dp_10"
android:text="@string/invalid_pin"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/input"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<EditText
android:id="@+id/input"
style="@style/LockKeyPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/dp_320"
android:lines="1"
android:layout_marginBottom="@dimen/dp_16"
android:autofillHints="password"
app:layout_constraintBottom_toTopOf="@id/button_one"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/password_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/dp_320"
android:maxWidth="@dimen/dp_1260"
android:lines="1"
android:ellipsize="marquee"
android:gravity="center"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="@id/input"
app:layout_constraintStart_toStartOf="@id/input"
app:layout_constraintTop_toTopOf="@id/input"
app:layout_constraintBottom_toBottomOf="@id/input"/>
<ImageView
android:id="@+id/clear_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_lock_key_close"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/input"
app:layout_constraintStart_toEndOf="@+id/password_display"
app:layout_constraintTop_toTopOf="@id/input" />
<TextView
android:id="@+id/button_one"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:layout_marginBottom="@dimen/dp_50"
android:text="1"
app:layout_constraintBottom_toTopOf="@id/button_four"
app:layout_constraintEnd_toStartOf="@id/button_two"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/input" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_one"
app:layout_constraintEnd_toStartOf="@id/button_one"
app:layout_constraintStart_toEndOf="@id/button_one" />
<TextView
android:id="@+id/button_two"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:layout_marginStart="@dimen/dp_80"
android:layout_marginEnd="@dimen/dp_80"
android:text="2"
app:layout_constraintBottom_toBottomOf="@id/button_one"
app:layout_constraintEnd_toStartOf="@id/button_three"
app:layout_constraintStart_toEndOf="@id/button_one"
app:layout_constraintTop_toTopOf="@id/button_one" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_two"
app:layout_constraintEnd_toStartOf="@id/button_two"
app:layout_constraintStart_toEndOf="@id/button_two" />
<TextView
android:id="@+id/button_three"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:text="3"
app:layout_constraintBottom_toBottomOf="@id/button_one"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_two"
app:layout_constraintTop_toTopOf="@id/button_one" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_three"
app:layout_constraintEnd_toStartOf="@id/button_three"
app:layout_constraintStart_toEndOf="@id/button_three" />
<TextView
android:id="@+id/button_four"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:layout_marginBottom="@dimen/dp_50"
android:text="4"
app:layout_constraintBottom_toTopOf="@id/button_seven"
app:layout_constraintEnd_toStartOf="@id/button_five"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_one" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_four"
app:layout_constraintEnd_toStartOf="@id/button_four"
app:layout_constraintStart_toEndOf="@id/button_four" />
<TextView
android:id="@+id/button_five"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:layout_marginStart="@dimen/dp_80"
android:layout_marginEnd="@dimen/dp_80"
android:text="5"
app:layout_constraintBottom_toBottomOf="@id/button_four"
app:layout_constraintEnd_toStartOf="@id/button_six"
app:layout_constraintStart_toEndOf="@id/button_four"
app:layout_constraintTop_toTopOf="@id/button_four" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_five"
app:layout_constraintEnd_toStartOf="@id/button_five"
app:layout_constraintStart_toEndOf="@id/button_five" />
<TextView
android:id="@+id/button_six"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:text="6"
app:layout_constraintBottom_toBottomOf="@id/button_four"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_five"
app:layout_constraintTop_toTopOf="@id/button_four" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_six"
app:layout_constraintEnd_toStartOf="@id/button_six"
app:layout_constraintStart_toEndOf="@id/button_six" />
<TextView
android:id="@+id/button_seven"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:layout_marginBottom="@dimen/dp_50"
android:text="7"
app:layout_constraintBottom_toTopOf="@id/button_delete"
app:layout_constraintEnd_toStartOf="@id/button_eight"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_four" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_seven"
app:layout_constraintEnd_toStartOf="@id/button_seven"
app:layout_constraintStart_toEndOf="@id/button_seven" />
<TextView
android:id="@+id/button_eight"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:layout_marginStart="@dimen/dp_80"
android:layout_marginEnd="@dimen/dp_80"
android:text="8"
app:layout_constraintBottom_toBottomOf="@id/button_seven"
app:layout_constraintEnd_toStartOf="@id/button_nine"
app:layout_constraintStart_toEndOf="@id/button_seven"
app:layout_constraintTop_toTopOf="@id/button_seven" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_eight"
app:layout_constraintEnd_toStartOf="@id/button_eight"
app:layout_constraintStart_toEndOf="@id/button_eight" />
<TextView
android:id="@+id/button_nine"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:text="9"
app:layout_constraintBottom_toBottomOf="@id/button_seven"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_eight"
app:layout_constraintTop_toTopOf="@id/button_seven" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_nine"
app:layout_constraintEnd_toStartOf="@id/button_nine"
app:layout_constraintStart_toEndOf="@id/button_nine" />
<ImageView
android:id="@+id/button_delete"
style="@style/LockFab"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:scaleType="center"
android:src="@drawable/ic_lock_key_delete"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/button_zero"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button_seven" />
<TextView
android:id="@+id/button_zero"
style="@style/LockCircleButton"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:layout_marginStart="@dimen/dp_80"
android:layout_marginEnd="@dimen/dp_80"
android:text="0"
app:layout_constraintBottom_toBottomOf="@id/button_delete"
app:layout_constraintEnd_toStartOf="@id/button_enter"
app:layout_constraintStart_toEndOf="@id/button_delete"
app:layout_constraintTop_toTopOf="@id/button_delete" />
<View
android:layout_width="@dimen/lock_key_line_width"
android:layout_height="@dimen/lock_key_line_height"
android:background="@color/lock_key_bottom_line"
app:layout_constraintBottom_toBottomOf="@id/button_zero"
app:layout_constraintEnd_toStartOf="@id/button_zero"
app:layout_constraintStart_toEndOf="@id/button_zero" />
<ImageView
android:id="@+id/button_enter"
style="@style/LockFab"
android:layout_width="@dimen/fab_icon_size"
android:layout_height="@dimen/fab_icon_size"
android:scaleType="center"
android:src="@drawable/ic_lock_key_check_selector"
app:layout_constraintBottom_toBottomOf="@id/button_delete"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/button_zero"
app:layout_constraintTop_toTopOf="@id/button_delete"
app:maxImageSize="@dimen/fab_size_36" />
</androidx.constraintlayout.widget.ConstraintLayout>
2. XML使用示例:
<com.your.app.view.LockKeypad
android:id="@+id/lock_keypad"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
3. Activity使用示例:
lock_keypad.onSuccessListener = { pin ->
lifecycleScope.launch {
try {
val user = userViewModel.getUserByPin(pin.toInt())
if (user != null) { //verify Success
lock_keypad.hideError()
sp.userId = user.userId
sp.userName = user.userName
lock_keypad.input.setText("")
//TODO 验证通过执行跳转页面
// findNavigate()
} else {
//TODO 验证不通过执行
lock_keypad.showError()
}
} catch (e: Exception) {
lock_keypad.showError()
Logger.e(TAG, "Error getting user from the db", e)
}
}
}