Last active
October 12, 2021 18:27
-
-
Save masoudkarimi/4f8b75ab3cc0c70d74f380e8b6885883 to your computer and use it in GitHub Desktop.
OTP EditText
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package mkn.libs.otp | |
import android.content.res.Resources | |
import kotlin.math.ceil | |
fun Int.dpf(): Float { | |
return this.dp().toFloat() | |
} | |
fun Float.dpf(): Float { | |
return this.dp().toFloat() | |
} | |
fun Int.dp(): Int { | |
return if (this == 0) { | |
0 | |
} else ceil((Resources.getSystem().displayMetrics.density * this).toDouble()).toInt() | |
} | |
fun Float.dp(): Int { | |
return if (this == 0f) { | |
0 | |
} else ceil((Resources.getSystem().displayMetrics.density * this).toDouble()).toInt() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package mkn.libs.otp | |
import android.content.Context | |
import android.graphics.Canvas | |
import android.graphics.Color | |
import android.graphics.Paint | |
import android.graphics.RectF | |
import android.text.* | |
import android.util.TypedValue | |
import androidx.appcompat.widget.AppCompatEditText | |
import androidx.core.content.res.ResourcesCompat | |
class OTPEditText @JvmOverloads constructor( | |
context: Context, | |
attributeSet: AttributeSet? = null | |
) : AppCompatEditText(context), TextWatcher { | |
private var mWidth: Int = 0 | |
var otpCount = 6 | |
set(value) { | |
field = value | |
filters = arrayOf(InputFilter.LengthFilter(value)) | |
} | |
private var paddingPx: Int = 0 | |
private var internalStopFormatFlag: Boolean = false | |
private val spaceBetweenCharactersEm = 2 | |
private val emSize: Float | |
private var lineWidth = 0f | |
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG) | |
private var activeColor = Color.argb(120, 255, 255, 255) | |
private var normalColor = Color.argb(30, 255, 255, 255) | |
private var successColor = Color.GREEN | |
private val lineRect = Array(otpCount) { RectF() } | |
init { | |
inputType = InputType.TYPE_CLASS_NUMBER | |
setTextSize(TypedValue.COMPLEX_UNIT_SP, 26f) | |
filters = arrayOf(InputFilter.LengthFilter(otpCount)) | |
typeface = ResourcesCompat.getFont(context, R.font.irsansbold_monospace) // use your font here | |
addTextChangedListener(this) | |
emSize = paint.measureText("1") | |
lineWidth = emSize * 2f | |
paddingPx = (spaceBetweenCharactersEm * emSize).toInt() | |
setPadding( | |
paddingPx, | |
0, | |
paddingPx, | |
0 | |
) | |
background = null | |
linePaint.apply { | |
style = Paint.Style.FILL | |
} | |
} | |
override fun onSelectionChanged(selStart: Int, selEnd: Int) { | |
val text: CharSequence? = text | |
if (text != null) { | |
if (selStart != text.length || selEnd != text.length) { | |
setSelection(text.length, text.length) | |
return | |
} | |
} | |
super.onSelectionChanged(selStart, selEnd) | |
} | |
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { | |
val charSpaceWidth = emSize * spaceBetweenCharactersEm | |
val allSpacesWidth = (otpCount - 1) * charSpaceWidth | |
val allCharactersWidth = otpCount * emSize | |
val horizontalPadding = paddingLeft + paddingRight | |
mWidth = allCharactersWidth.toInt() + allSpacesWidth.toInt() + horizontalPadding | |
super.onMeasure( | |
MeasureSpec.makeMeasureSpec(mWidth, MeasureSpec.EXACTLY), | |
MeasureSpec.makeMeasureSpec(56.dp(), MeasureSpec.EXACTLY) | |
) | |
} | |
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { | |
super.onSizeChanged(w, h, oldw, oldh) | |
lineRect[0].set( | |
(paddingLeft + emSize.div(2) - lineWidth.div(2)), | |
height - 10.dpf(), | |
paddingLeft + emSize.div(2) - lineWidth.div(2) + lineWidth, | |
height - 7.dpf() | |
) | |
lineRect.forEachIndexed { index, rectF -> | |
if (index > 0) { | |
rectF.set( | |
lineRect[index - 1].right - lineWidth.div(2) - emSize.div(2) + emSize.plus( | |
paddingPx | |
) + emSize.div(2) - lineWidth.div(2), | |
height - 10.dpf(), | |
lineRect[index - 1].right - lineWidth.div(2) - emSize.div(2) + emSize.plus( | |
paddingPx | |
) + emSize.div(2) - lineWidth.div(2) + lineWidth, | |
height - 7.dpf() | |
) | |
} | |
} | |
} | |
override fun onDraw(canvas: Canvas?) { | |
super.onDraw(canvas) | |
lineRect.forEachIndexed { index, rectF -> | |
if (index <= selectionStart) { | |
linePaint.color = activeColor | |
} else { | |
linePaint.color = normalColor | |
} | |
if (selectionStart == otpCount) { | |
linePaint.color = successColor | |
} | |
canvas?.drawRoundRect(rectF, 6.dpf(), 6.dpf(), linePaint) | |
} | |
} | |
override fun afterTextChanged(s: Editable?) { | |
if (internalStopFormatFlag || s == null) { | |
return | |
} | |
internalStopFormatFlag = true | |
formatText(s, paddingPx) | |
internalStopFormatFlag = false | |
} | |
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} | |
override fun onTextChanged( | |
text: CharSequence?, | |
start: Int, | |
lengthBefore: Int, | |
lengthAfter: Int | |
) { | |
} | |
private fun formatText( | |
ccNumber: Editable, | |
paddingPx: Int | |
) { | |
val textLength = ccNumber.length | |
// first remove any previous span | |
val spans = ccNumber.getSpans(0, ccNumber.length, RightPaddingSpan::class.java) | |
for (i in spans.indices) { | |
ccNumber.removeSpan(spans[i]) | |
} | |
for (i in 0 until textLength) { | |
val padding = if (i != otpCount - 1) paddingPx else 0 | |
val marginSPan = RightPaddingSpan( | |
padding | |
) | |
ccNumber.setSpan(marginSPan, i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package mkn.libs.otp | |
import android.graphics.Canvas | |
import android.graphics.Paint | |
import android.text.style.ReplacementSpan | |
class RightPaddingSpan(private val mPadding: Int) : ReplacementSpan() { | |
override fun getSize( | |
paint: Paint, | |
text: CharSequence, | |
start: Int, | |
end: Int, | |
fm: Paint.FontMetricsInt? | |
): Int { | |
val widths = FloatArray(end - start) | |
paint.getTextWidths(text, start, end, widths) | |
val sum = widths.sumBy { | |
it.toInt() | |
} | |
return sum + mPadding | |
} | |
override fun draw( | |
canvas: Canvas, | |
text: CharSequence, | |
start: Int, | |
end: Int, | |
x: Float, | |
top: Int, | |
y: Int, | |
bottom: Int, | |
paint: Paint | |
) { | |
canvas.drawText(text, start, end, x, y.toFloat(), paint) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment