package com.yidianling.user.widget.PinField import `in`.srain.cube.util.LocalDisplay.dp2px import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import androidx.core.content.ContextCompat import androidx.appcompat.widget.AppCompatEditText import android.text.InputFilter import android.text.method.PasswordTransformationMethod import android.text.method.TransformationMethod import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import com.yidianling.common.tools.LogUtil import com.yidianling.user.R /** * Created by poovam-5255 on 3/3/2018. * View where all the magic happens */ open class PinField : AppCompatEditText { private val defaultWidth = dp2px(60f) companion object { const val DEFAULT_DISTANCE_IN_BETWEEN = -1f } protected var distanceInBetween: Float = DEFAULT_DISTANCE_IN_BETWEEN set(value) { field = value requestLayout() invalidate() } var numberOfFields = 4 set(value) { field = value limitCharsToNoOfFields() invalidate() } protected var singleFieldWidth = 0 var lineThickness = dp2px(1.0f) set(value) { field = value fieldPaint.strokeWidth = field.toFloat() highlightPaint.strokeWidth = highLightThickness.toFloat() invalidate() } var fieldColor = ContextCompat.getColor(context, R.color.user_inactivePinFieldColor) set(value) { field = value fieldPaint.color = field invalidate() } var highlightPaintColor = ContextCompat.getColor(context, R.color.user_pinFieldLibraryAccent) set(value) { field = value highlightPaint.color = field invalidate() } var isCursorEnabled = false set(value) { field = value invalidate() } protected var fieldPaint = Paint() protected var textPaint = Paint() protected var hintPaint = Paint() protected var highlightPaint = Paint() protected var yPadding = dp2px(10f) var highlightSingleFieldType = HighlightType.ALL_FIELDS private var lastCursorChangeState: Long = -1 private var cursorCurrentVisible = true private val cursorTimeout = 500L // public var fieldBgColor=0 var isCustomBackground = false set(value) { if (!value) { setBackgroundResource(R.color.user_pinFieldLibraryTransparent) } field = value } var highLightThickness = lineThickness get() { return (lineThickness + lineThickness * 0.7f).toInt() } var onTextCompleteListener: OnTextCompleteListener? = null var fieldBgColor = ContextCompat.getColor(context, R.color.user_pinFieldLibraryAccent) set(value) { field = value fieldBgPaint.color = fieldBgColor invalidate() } var fieldBgPaint = Paint() init { limitCharsToNoOfFields() setWillNotDraw(false) maxLines = 1 setSingleLine(true) fieldPaint.color = fieldColor fieldPaint.isAntiAlias = true fieldPaint.style = Paint.Style.STROKE fieldPaint.strokeWidth = lineThickness.toFloat() textPaint.color = currentTextColor textPaint.isAntiAlias = true textPaint.textSize = textSize textPaint.textAlign = Paint.Align.CENTER textPaint.style = Paint.Style.FILL hintPaint.color = hintTextColors.defaultColor hintPaint.isAntiAlias = true hintPaint.textSize = textSize hintPaint.textAlign = Paint.Align.CENTER hintPaint.style = Paint.Style.FILL highlightPaint = Paint(fieldPaint) highlightPaint.color = highlightPaintColor highlightPaint.strokeWidth = highLightThickness.toFloat() fieldBgColor = Color.TRANSPARENT fieldBgPaint.style = Paint.Style.FILL } constructor(context: Context) : super(context) constructor(context: Context, attr: AttributeSet) : super(context, attr) { initParams(attr) } constructor(context: Context, attr: AttributeSet, defStyle: Int) : super( context, attr, defStyle ) { initParams(attr) } private fun initParams(attr: AttributeSet) { val a = context.theme.obtainStyledAttributes(attr, R.styleable.User_PinField, 0, 0) try { numberOfFields = a.getInt(R.styleable.User_PinField_User_noOfFields, numberOfFields) lineThickness = a.getDimension(R.styleable.User_PinField_User_lineThickness, lineThickness.toFloat()).toInt() distanceInBetween = a.getDimension( R.styleable.User_PinField_User_distanceInBetween, DEFAULT_DISTANCE_IN_BETWEEN ) fieldColor = a.getColor(R.styleable.User_PinField_User_fieldColor, fieldColor) highlightPaintColor = a.getColor(R.styleable.User_PinField_User_highlightColor, highlightPaintColor) isCustomBackground = a.getBoolean(R.styleable.User_PinField_User_isCustomBackground, false) isCursorEnabled = a.getBoolean(R.styleable.User_PinField_User_isCursorEnabled, false) highlightSingleFieldType = if (a.getBoolean( R.styleable.User_PinField_User_highlightEnabled, true ) ) HighlightType.ALL_FIELDS else HighlightType.NO_FIELDS highlightSingleFieldType = if (a.getBoolean( R.styleable.User_PinField_User_highlightSingleFieldMode, false ) ) HighlightType.CURRENT_FIELD else HighlightType.ALL_FIELDS highlightSingleFieldType = HighlightType.getEnum( a.getInt( R.styleable.User_PinField_User_highlightType, highlightSingleFieldType.code ) ) fieldBgColor = a.getColor(R.styleable.User_PinField_User_fieldBgColor, fieldBgColor) textPaint.typeface = typeface } finally { a.recycle() } } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val width = getViewWidth(defaultWidth * numberOfFields, widthMeasureSpec) singleFieldWidth = width / numberOfFields setMeasuredDimension(width, getViewHeight(singleFieldWidth, heightMeasureSpec)) } protected open fun getViewWidth(desiredWidth: Int, widthMeasureSpec: Int): Int { val widthMode = MeasureSpec.getMode(widthMeasureSpec) val widthSize = MeasureSpec.getSize(widthMeasureSpec) //Measure Width return when (widthMode) { MeasureSpec.EXACTLY -> widthSize MeasureSpec.AT_MOST -> Math.min(desiredWidth, widthSize) MeasureSpec.UNSPECIFIED -> desiredWidth else -> desiredWidth } } protected open fun getViewHeight(desiredHeight: Int, heightMeasureSpec: Int): Int { val heightMode = MeasureSpec.getMode(heightMeasureSpec) val heightSize = MeasureSpec.getSize(heightMeasureSpec) //Measure Height return when (heightMode) { MeasureSpec.EXACTLY -> heightSize MeasureSpec.AT_MOST -> Math.min(desiredHeight, heightSize) MeasureSpec.UNSPECIFIED -> desiredHeight else -> desiredHeight } } override fun onSelectionChanged(selStart: Int, selEnd: Int) { this.setSelection(this.text!!.length) } final override fun setWillNotDraw(willNotDraw: Boolean) { super.setWillNotDraw(willNotDraw) } override fun onCheckIsTextEditor(): Boolean { return true } protected open fun getDefaultDistanceInBetween(): Float { return (singleFieldWidth / (numberOfFields - 1)).toFloat() } private fun limitCharsToNoOfFields() { val filterArray = arrayOfNulls<InputFilter>(1) filterArray[0] = InputFilter.LengthFilter(numberOfFields) filters = filterArray } override fun onTextChanged( text: CharSequence?, start: Int, lengthBefore: Int, lengthAfter: Int ) { super.onTextChanged(text, start, lengthBefore, lengthAfter) if (text != null && text.length == numberOfFields) { val shouldCloseKeyboard = onTextCompleteListener?.onTextComplete(text.toString()) ?: false if (shouldCloseKeyboard) { val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.hideSoftInputFromWindow(windowToken, 0) } } } protected fun shouldDrawHint(): Boolean { return (text!!.isEmpty() || text!!.isBlank()) && !isFocused && (hint != null && hint.isNotBlank() && hint.isNotEmpty()) } protected fun drawCursor(canvas: Canvas?, x: Float, y1: Float, y2: Float, paint: Paint) { if (System.currentTimeMillis() - lastCursorChangeState > 500) { cursorCurrentVisible = !cursorCurrentVisible lastCursorChangeState = System.currentTimeMillis() } if (cursorCurrentVisible) { canvas?.drawLine(x, y1, x, y2, paint) } postInvalidateDelayed(cursorTimeout) } final override fun setBackgroundResource(resId: Int) { super.setBackgroundResource(resId) } protected fun highlightNextField(): Boolean { return highlightSingleFieldType == HighlightType.CURRENT_FIELD } protected fun highlightCompletedFields(): Boolean { return highlightSingleFieldType == HighlightType.COMPLETED_FIELDS } protected fun highlightAllFields(): Boolean { return highlightSingleFieldType == HighlightType.ALL_FIELDS } protected fun highlightNoFields(): Boolean { return highlightSingleFieldType == HighlightType.NO_FIELDS } protected fun highlightLogic(currentPosition: Int, textLength: Int?, onHighlight: () -> Unit) { if (hasFocus() && !highlightNoFields()) { when { highlightNextField() && currentPosition == textLength ?: 0 -> { onHighlight.invoke() } highlightCompletedFields() && currentPosition < textLength ?: 0 -> { onHighlight.invoke() } } } } //There is a issue where android transformation method is not set to password so as a work around //the transformation method used is hardcoded //the check condition used in isPassword() is taken from TextView.java constructor protected fun getCharAt(i: Int): Char? { return getPinFieldTransformation().getTransformation(text, this)?.getOrNull(i) ?: text?.getOrNull(i) } private fun getPinFieldTransformation(): TransformationMethod { if (isPassword()) { return PasswordTransformationMethod.getInstance() } return transformationMethod } private fun isPassword(): Boolean { val variation = inputType and (EditorInfo.TYPE_MASK_CLASS or EditorInfo.TYPE_MASK_VARIATION) val passwordInputType = (variation == EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD) val webPasswordInputType = (variation == EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD) val numberPasswordInputType = (variation == EditorInfo.TYPE_CLASS_NUMBER or EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD) return passwordInputType || webPasswordInputType || numberPasswordInputType } interface OnTextCompleteListener { /** * @return return true if keyboard should be closed after text is entered */ fun onTextComplete(enteredText: String): Boolean } } enum class HighlightType(val code: Int) { ALL_FIELDS(0), CURRENT_FIELD(1), COMPLETED_FIELDS(2), NO_FIELDS(3); companion object { fun getEnum(code: Int): HighlightType { for (type in HighlightType.values()) { if (type.code == code) { return type } } return ALL_FIELDS } } }