I'm working on an app screen where users will have to enter an OTP code they will receive via SMS. The look I am going for is the following (note that the rest of the screen looks as expected and has no issues so I'll only be showing what isn't working):
As you can see, this design features individual rounded rectangles that contain single characters from the OTP code. All colours and fonts are unimportant in regards to my issue (since they can be modified in the resource xml files.
My current code doesn not however render out as I expected and looks like the following:
Here is the kotlin code associated to the view and the associated attrs.xml and dimens.xml files:
OtpTextField.kt
package com.example.myapp.widget
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.PaintDrawable
import android.text.InputFilter
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import com.example.myapp.Event
import com.example.myapp.util.extension.dismissKeyboard
import com.example.myapp.R
import com.example.myapp.databinding.ViewOtpEdittextBinding
class OtpTextField(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
private var binding: ViewOtpEdittextBinding =
ViewOtpEdittextBinding.inflate(LayoutInflater.from(context), this)
var length: Int = -1
set(value) {
if (field != value) {
field = value
applyLength()
}
}
var otp: String
get() = binding.otpInput.text?.toString().orEmpty()
set(value) {
binding.otpInput.setText(value)
}
var cellWidth: Int = -1
var cellHeight: Int = -1
var cellSpacing: Int = -1
var cellCornerRadius: Int = -1
var cellBackgroundColor: Int = -1
private var otpDisplayTexts: List<TextView> = emptyList()
private var _otpEntered = MediatorLiveData<Event<Unit>>()
val otpEntered: LiveData<Event<Unit>> = _otpEntered
init {
applyUserAttributes(context, attrs)
bindEditTextToDisplay()
}
private fun applyUserAttributes(context: Context, attrs: AttributeSet?) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.OtpTextField, 0, 0)
length =
typedArray.getInt(R.styleable.OtpTextField_otp_length, DEFAULT_OTP_LENGTH)
cellWidth = typedArray.getDimension(
R.styleable.OtpTextField_cell_width,
resources.getDimension(R.dimen.otp_cell_width)
).toInt()
cellHeight = typedArray.getDimension(
R.styleable.OtpTextField_cell_height,
resources.getDimension(R.dimen.otp_cell_height)
).toInt()
cellSpacing = typedArray.getDimension(
R.styleable.OtpTextField_cell_spacing,
resources.getDimension(R.dimen.otp_cell_spacing)
).toInt()
cellCornerRadius = typedArray.getDimension(
R.styleable.OtpTextField_cell_corner_radius,
resources.getDimension(R.dimen.otp_cell_corner_radius)
).toInt()
val defaultBackgroundColor = resources.getColor(R.color.secondary, context.theme)
cellBackgroundColor = typedArray.getColor(
R.styleable.OtpTextField_cell_background_color,
Color.argb(
@Suppress("MagicNumber")
26,
Color.red(defaultBackgroundColor),
Color.green(defaultBackgroundColor),
Color.blue(defaultBackgroundColor)
)
)
typedArray.recycle()
}
private fun applyLength() {
binding.otpInput.filters = arrayOf(InputFilter.LengthFilter(length))
val linearRoot = binding.otpDisplayedContentLinear
linearRoot.removeAllViews()
otpDisplayTexts = List(length) {
applyCellAttributes(linearRoot.context)
.also {
linearRoot.addView(it)
}
}
}
private fun applyCellAttributes(context: Context): TextView {
return TextView(context).apply {
layoutParams = LinearLayout.LayoutParams(cellWidth, cellHeight, 1.0f).apply {
setMargins(cellSpacing / 2, 0, cellSpacing / 2, 0)
}
background = PaintDrawable(cellBackgroundColor).apply {
setCornerRadius(cellCornerRadius.toFloat())
}
textSize = resources.getDimension(R.dimen.text_size_h5)
setTypeface(typeface, Typeface.BOLD)
setTextColor(resources.getColor(R.color.text_color_primary, context.theme))
includeFontPadding = false
gravity = Gravity.CENTER
}
}
private fun bindEditTextToDisplay() {
binding.otpInput.addTextChangedListener(afterTextChanged = { newText ->
otpDisplayTexts.forEach { it.text = "" }
newText?.toList()?.forEachIndexed { index, char ->
otpDisplayTexts.getOrNull(index)?.text = "$char"
}
if (newText?.length == length) {
binding.root.dismissKeyboard()
_otpEntered.value = Event(Unit)
}
})
}
companion object {
private const val DEFAULT_OTP_LENGTH = 6
}
}
view_otp_edittext.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:parentTag="android.widget.FrameLayout">
<!--
This EditText is hidden from the user as the content is displayed somewhere else
Only use it to manage keyboard management, focus etc ...
-->
<com.example.myapp.widget.NonSelectableEditText
android:id="@+id/otp_input"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null"
android:textColor="@color/transparent" />
<LinearLayout
android:id="@+id/otp_displayed_content_linear"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
</LinearLayout>
</merge>
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="OtpTextField">
<attr name="otp_length" format="integer" />
<attr name="cell_width" format="dimension" />
<attr name="cell_height" format="dimension" />
<attr name="cell_corner_radius" format="dimension" />
<attr name="cell_spacing" format="dimension" />
<attr name="cell_background_color" format="reference|color"/>
</declare-styleable>
</resources>
dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- OTP Text Field default dimensions -->
<dimen name="otp_cell_width">56dp</dimen>
<dimen name="otp_cell_height">60dp</dimen>
<dimen name="otp_cell_corner_radius">12dp</dimen>
<dimen name="otp_cell_spacing">11dp</dimen>
</resources>
As you can see, the OtpTextField.kt file has the correct declarations and definitions for the look I am going for but the white background is somehow present and I am unsure as to why. Any help would be appreaciated!

