Custom view has background that hides the actual aesthetics

Viewed 60

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):

OTP input design

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:

OTP input render

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!

0 Answers
Related