-
-
Save alvindizon/1ee479494356852b20e24f32eb2f9625 to your computer and use it in GitHub Desktop.
A custom view to handle otp input in multiple edittexts. It will move focus to next edittext, if available, when user enters otp and it will move focus to the previous edittext, if available, when user deletes otp. It will also delegate the paste option, if user long presses and pastes a string into the otp input.
This file contains 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
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
... | |
<declare-styleable name="OTPView"> | |
<!-- Defines the next view to give focus to when the text is filled.--> | |
<attr name="nextView" format="reference"/> | |
<!-- Defines the previous view to give focus to when backButton is pressed.--> | |
<attr name="prevView" format="reference"/> | |
</declare-styleable> | |
... | |
</resources> |
This file contains 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 com.yourapp.ui | |
import android.content.ClipboardManager | |
import android.content.Context | |
import android.graphics.Rect | |
import android.util.AttributeSet | |
import android.view.KeyEvent | |
import android.view.View | |
import androidx.appcompat.widget.AppCompatEditText | |
import androidx.core.widget.doAfterTextChanged | |
import com.yourapp.design.R | |
class VerificationCodeView : AppCompatEditText { | |
private var nextViewId = 0 | |
private var previousViewId = 0 | |
private var onPasteListener: ((String) -> Unit)? = null | |
private var nextView: View? = null | |
get() { | |
if (field != null) { | |
return field | |
} | |
if (nextViewId != NO_ID && parent is View) { | |
field = (parent as View).findViewById(nextViewId) | |
return field | |
} | |
return null | |
} | |
private var previousView: View? = null | |
get() { | |
if (field != null) { | |
return field | |
} | |
if (previousViewId != NO_ID && parent is View) { | |
field = (parent as View).findViewById(previousViewId) | |
return field | |
} | |
return null | |
} | |
constructor(context: Context) : super(context) | |
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { | |
init(context, attrs) | |
} | |
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( | |
context, | |
attrs, | |
defStyleAttr | |
) { | |
init(context, attrs) | |
} | |
fun setPasteListener(listener: ((String) -> Unit)) { | |
onPasteListener = listener | |
} | |
override fun onTextContextMenuItem(id: Int): Boolean { | |
return if (id == android.R.id.paste) { | |
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager | |
// Assumes that this application can only handle one item at a time. | |
val item = clipboard.primaryClip!!.getItemAt(0) | |
item.text?.run { | |
onPasteListener?.invoke(toString()) | |
true | |
} ?: false | |
} else { | |
super.onTextContextMenuItem(id) | |
} | |
} | |
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { | |
super.onFocusChanged(focused, direction, previouslyFocusedRect) | |
if (focused) text?.length?.let { setSelection(it) } | |
} | |
private fun init(context: Context, attrs: AttributeSet?) { | |
context.obtainStyledAttributes(attrs, R.styleable.VerificationCodeView).apply { | |
nextViewId = getResourceId(R.styleable.VerificationCodeView_nextView, NO_ID) | |
previousViewId = getResourceId(R.styleable.VerificationCodeView_previousView, NO_ID) | |
recycle() | |
} | |
setOnKeyListener { _, keyCode, event -> | |
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DEL && text?.isEmpty() == true) { | |
previousView?.requestFocus() | |
true | |
} else { | |
false | |
} | |
} | |
doAfterTextChanged { if (it?.isNotEmpty() == true) nextView?.requestFocus() } | |
// setOnKeyListener { _, keyCode, event -> | |
// if (event.action != KeyEvent.ACTION_DOWN) { | |
// return@setOnKeyListener true | |
// } | |
// if (keyCode == KeyEvent.KEYCODE_DEL && text?.isEmpty() == true) { | |
// previousView?.requestFocus() | |
// return@setOnKeyListener true | |
// } | |
// false | |
// } | |
// | |
// doAfterTextChanged { editable -> | |
// if (editable?.length == 1 && nextView != null) { | |
// nextView?.requestFocus() | |
// } else if (editable?.isEmpty() == true && previousView != null) { | |
// previousView?.requestFocus() | |
// } | |
// } | |
} | |
companion object { | |
private const val NO_ID = -1 | |
} | |
} |
This file contains 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
... | |
private val editTextList by lazy { | |
with(binding.codeContainer) { | |
listOf(inputEditText1, inputEditText2, inputEditText3, inputEditText4) | |
} | |
} | |
... | |
editTextList.forEachIndexed { i, editText -> | |
// this gets the copied code from clipboard and distributes its digits to each edittext | |
editText.setPasteListener { pastedText -> | |
if (pastedText.length == 4) { | |
pastedText.forEachIndexed { index, c -> | |
editTextList[index].setText(c.toString()) | |
} | |
// set cursor position in last EditText | |
editTextList.last().setSelection(1) | |
} | |
} | |
editText.doAfterTextChanged { | |
if (it?.isNotEmpty() == true) { | |
val code = editTextList.fold(StringBuilder()) { sb, et -> | |
sb.append(et.text) | |
}.toString() | |
if (code.length == 4) { | |
// TODO send code here | |
} | |
} | |
} | |
} | |
... |
This file contains 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
... | |
<LinearLayout | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:orientation="horizontal"> | |
<your_package_name.OTPEditText | |
android:id="@+id/otp1" | |
android:layout_width="0dp" | |
android:layout_weight="1" | |
android:layout_height="wrap_content" | |
android:inputType="number" | |
android:imeOptions="actionNext" | |
android:maxLength="1" | |
android:padding="12dp" | |
android:focusable="true" | |
app:nextView="@id/otp2"/> | |
<Space | |
android:layout_width="12dp" | |
android:layout_height="wrap_content" /> | |
<your_package_name.OTPEditText | |
android:id="@+id/otp2" | |
android:layout_width="0dp" | |
android:layout_weight="1" | |
android:layout_height="wrap_content" | |
android:inputType="number" | |
android:imeOptions="actionNext" | |
android:maxLength="1" | |
android:padding="12dp" | |
android:focusable="true" | |
app:prevView="@id/otp1" | |
app:nextView="@id/otp3" /> | |
<Space | |
android:layout_width="12dp" | |
android:layout_height="wrap_content" /> | |
<your_package_name.OTPEditText | |
android:id="@+id/otp3" | |
android:layout_width="0dp" | |
android:layout_weight="1" | |
android:layout_height="wrap_content" | |
android:inputType="number" | |
android:imeOptions="actionNext" | |
android:maxLength="1" | |
android:padding="12dp" | |
android:focusable="true" | |
app:prevView="@id/otp2" | |
app:nextView="@id/otp4" /> | |
<Space | |
android:layout_width="12dp" | |
android:layout_height="wrap_content" /> | |
<your_package_name.OTPEditText | |
android:id="@+id/otp4" | |
android:layout_width="0dp" | |
android:layout_weight="1" | |
android:layout_height="wrap_content" | |
android:inputType="number" | |
android:imeOptions="actionDone" | |
android:maxLength="1" | |
android:padding="12dp" | |
android:focusable="true" | |
app:prevView="@id/otp3" /> | |
</LinearLayout> | |
... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment