

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.text.InputFilter
import android.text.InputType
import android.util.AttributeSet
import android.util.TypedValue
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.animation.ObjectAnimator
import android.animation.AnimatorListenerAdapter
import android.view.animation.CycleInterpolator
class VerificationCodeView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
interface OnVerificationCodeCompleteListener {
fun onVerificationCodeComplete(code: String)
}
fun showError() {
isError = true
invalidate()
startShakeAnimation()
}
private fun clearError() {
if (!isError) return
isError = false
invalidate()
}
fun setOnShakeAnimationEndListener(listener: () -> Unit) {
onShakeAnimationEndListener = listener
}
private fun startShakeAnimation() {
shakeAnimator?.cancel()
shakeAnimator = ObjectAnimator.ofFloat(this, "translationX", 0f, 25f).apply {
duration = 500
interpolator = CycleInterpolator(3f)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
onShakeAnimationEndListener?.invoke()
}
})
}
shakeAnimator?.start()
}
private var boxCount = 4
private var boxSize = dpToPx(50f)
private var boxMargin = dpToPx(10f)
private var boxCornerRadius = dpToPx(8f)
private var boxBorderWidth = dpToPx(2f)
private var boxBorderColor = Color.parseColor("#CCCCCC")
private var boxFocusedBorderColor = Color.parseColor("#009688")
private var boxErrorBorderColor = Color.parseColor("#FF0000")
private var boxBackgroundColor = Color.parseColor("#FFFFFF")
private var textColor = Color.parseColor("#000000")
private var textSize = spToPx(18f)
private var isError = false
private var shakeAnimator: ObjectAnimator? = null
private var onShakeAnimationEndListener: (() -> Unit)? = null
private val digitBoxes = ArrayList<DigitBox>()
private var verificationCodeCompleteListener: OnVerificationCodeCompleteListener? = null
init {
orientation = HORIZONTAL
createDigitBoxes()
}
private fun createDigitBoxes() {
removeAllViews()
digitBoxes.clear()
for (i in 0 until boxCount) {
val digitBox = DigitBox(context)
digitBox.id = View.generateViewId()
digitBox.setTextColor(textColor)
digitBox.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
digitBox.setBackgroundColor(Color.TRANSPARENT)
digitBox.inputType = InputType.TYPE_CLASS_NUMBER
digitBox.filters = arrayOf(InputFilter.LengthFilter(1))
digitBox.maxLines = 1
digitBox.isCursorVisible = false
digitBox.imeOptions = EditorInfo.IME_ACTION_NEXT
val params = LayoutParams(
boxSize.toInt(),
boxSize.toInt()
)
if (i > 0) {
params.leftMargin = boxMargin.toInt()
}
digitBox.layoutParams = params
digitBox.setOnFocusChangeListener { _, _ ->
digitBox.invalidate()
}
digitBox.setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
if (digitBox.text.isNullOrEmpty() && i > 0) {
clearError()
digitBoxes[i - 1].requestFocus()
digitBoxes[i - 1].text?.clear()
return@setOnKeyListener true
}
}
false
}
digitBox.addTextChangedListener(object : android.text.TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: android.text.Editable?) {
if (s?.length == 1) {
if (i < boxCount - 1) {
digitBoxes[i + 1].requestFocus()
} else {
checkCodeCompletion()
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(digitBox.windowToken, 0)
}
}
}
})
digitBoxes.add(digitBox)
addView(digitBox)
}
}
private fun checkCodeCompletion() {
val code = getCode()
if (code.length == boxCount) {
verificationCodeCompleteListener?.onVerificationCodeComplete(code)
}
}
fun getCode(): String {
val codeBuilder = StringBuilder()
for (digitBox in digitBoxes) {
codeBuilder.append(digitBox.text.toString())
}
return codeBuilder.toString()
}
fun setCode(code: String) {
clearCode()
val codeLength = minOf(code.length, boxCount)
for (i in 0 until codeLength) {
digitBoxes[i].setText(code[i].toString())
}
if (code.length < boxCount) {
digitBoxes[code.length].requestFocus()
} else {
digitBoxes.last().requestFocus()
}
}
fun clearCode() {
for (digitBox in digitBoxes) {
digitBox.text?.clear()
}
digitBoxes.firstOrNull()?.requestFocus()
}
fun setOnVerificationCodeCompleteListener(listener: OnVerificationCodeCompleteListener) {
verificationCodeCompleteListener = listener
}
fun setBoxCount(count: Int) {
if (count > 0 && count != boxCount) {
boxCount = count
createDigitBoxes()
}
}
fun setBoxSize(size: Float) {
boxSize = dpToPx(size)
requestLayout()
}
fun setBoxMargin(margin: Float) {
boxMargin = dpToPx(margin)
requestLayout()
}
fun setBoxCornerRadius(radius: Float) {
boxCornerRadius = dpToPx(radius)
invalidate()
}
fun setBoxBorderWidth(width: Float) {
boxBorderWidth = dpToPx(width)
invalidate()
}
fun setBoxBorderColor(color: Int) {
boxBorderColor = color
invalidate()
}
fun setBoxFocusedBorderColor(color: Int) {
boxFocusedBorderColor = color
invalidate()
}
fun setBoxBackgroundColor(color: Int) {
boxBackgroundColor = color
invalidate()
}
fun setDigitTextColor(color: Int) {
textColor = color
for (digitBox in digitBoxes) {
digitBox.setTextColor(color)
}
}
fun setDigitTextSize(size: Float) {
textSize = spToPx(size)
for (digitBox in digitBoxes) {
digitBox.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
}
}
private fun dpToPx(dp: Float): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
resources.displayMetrics
)
}
private fun spToPx(sp: Float): Float {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
sp,
resources.displayMetrics
)
}
private inner class DigitBox(context: Context) : androidx.appcompat.widget.AppCompatEditText(context) {
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val textBounds = Rect()
init {
borderPaint.style = Paint.Style.STROKE
backgroundPaint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
val width = width.toFloat()
val height = height.toFloat()
backgroundPaint.color = boxBackgroundColor
canvas.drawRoundRect(0f, 0f, width, height, boxCornerRadius, boxCornerRadius, backgroundPaint)
borderPaint.color = when {
isError -> boxErrorBorderColor
isFocused -> boxFocusedBorderColor
else -> boxBorderColor
}
borderPaint.strokeWidth = boxBorderWidth
canvas.drawRoundRect(
boxBorderWidth / 2,
boxBorderWidth / 2,
width - boxBorderWidth / 2,
height - boxBorderWidth / 2,
boxCornerRadius,
boxCornerRadius,
borderPaint
)
val text = text.toString()
if (text.isNotEmpty()) {
val textPaint = paint
textPaint.getTextBounds(text, 0, text.length, textBounds)
val textX = (width - textBounds.width()) / 2
val textY = height / 2 + textBounds.height() / 2
canvas.drawText(text, textX, textY, textPaint)
}
}
}
}
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class VerificationCodeActivity : AppCompatActivity() {
private lateinit var verificationCodeView: VerificationCodeView
private lateinit var clearButton: Button
private lateinit var fillButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_verification_code)
verificationCodeView = findViewById(R.id.verification_code_view)
clearButton = findViewById(R.id.btn_clear)
fillButton = findViewById(R.id.btn_fill)
verificationCodeView.setOnVerificationCodeCompleteListener(object : VerificationCodeView.OnVerificationCodeCompleteListener {
override fun onVerificationCodeComplete(code: String) {
Toast.makeText(this@VerificationCodeActivity, "Code entered: $code", Toast.LENGTH_SHORT).show()
}
})
clearButton.setOnClickListener {
verificationCodeView.clearCode()
}
fillButton.setOnClickListener {
verificationCodeView.setCode("1234")
verificationCodeView.showError()
}
}
}