Consider that:

  • Al2TextView inherits from TextView
  • Al2TextInputLayout inherits from TextInputLayout
  • Al2TextInputEditText inherits from TextInputEditText

When entering text into the second field (textinput_phone), the "Clear Text" icon (the X) appears and adjusts the height of the component, misaligning it with the first field (textinput_areacode).

`<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <com.rga.altwo.wallet.system.common.component.Al2TextView
        android:id="@+id/textinput_placeholder"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Teléfono celular"
        android:textAppearance="@style/TextEyebrowInput"
        android:textColor="@color/al2_black"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:textAllCaps="true"
        tools:ignore="HardcodedText" />

    <com.rga.altwo.wallet.system.common.component.Al2TextInputLayout
        android:id="@+id/textinput_areacode"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        app:boxStrokeColor="@color/al2_light_grey"
        app:hintEnabled="false"
        app:layout_constraintEnd_toStartOf="@id/textinput_phone"
        app:layout_constraintHorizontal_chainStyle="spread_inside"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textinput_placeholder"
        app:prefixTextAppearance="@style/Text2">

        <com.rga.altwo.wallet.system.common.component.Al2TextInputEditText
            android:id="@+id/edittext_areacode"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:backgroundTint="@color/al2_light_grey"
            android:focusable="true"
            android:focusableInTouchMode="true"
            android:focusedByDefault="false"
            android:inputType="numberDecimal"
            android:digits="123456789"
            android:maxLength="5" />
    </com.rga.altwo.wallet.system.common.component.Al2TextInputLayout>

    <com.rga.altwo.wallet.system.common.component.Al2TextInputLayout
        android:id="@+id/textinput_phone"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        app:boxStrokeColor="@color/al2_light_grey"
        app:hintEnabled="false"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/textinput_areacode"
        app:layout_constraintTop_toBottomOf="@id/textinput_placeholder">

        <com.rga.altwo.wallet.system.common.component.Al2TextInputEditText
            android:id="@+id/edittext_phone"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:backgroundTint="@color/al2_light_grey"
            android:focusable="true"
            android:focusableInTouchMode="true"
            android:focusedByDefault="false"
            android:inputType="numberDecimal"
            android:maxLength="9" />
    </com.rga.altwo.wallet.system.common.component.Al2TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>`

Al2TextInputLayout class ->

typealias OnTextLinkClickListener = () -> Unit

@Suppress("MagicNumber")
enum class Type(val type: Int) {
    STANDARD(0),
    SEARCH(1),
    USER(2),
    PASSWORD(3),
}

@Suppress("MagicNumber")
enum class CaptionType(val type: Int) {
    STANDARD(0),
    SUCCESS(1),
    ERROR(2),
}

class Al2TextInputLayout : TextInputLayout {
    private var passwordVisible = false
    private var textLink: Al2TextView? = null
    private var textLabel: Al2TextView? = null
    private var onTextLinkClickListener: OnTextLinkClickListener? = null

    private val viewEnable: Boolean
        get() = isEnabled

    constructor(context: Context) : super(context) {
        init(null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    ) {
        init(attrs)
    }

    var defaultBoxStrokeColor: Int = 0
    private fun init(attrs: AttributeSet?) {
        defaultBoxStrokeColor = boxStrokeColor
        ContextCompat.getColorStateList(context, R.color.textinput_background)?.let {
            setBoxBackgroundColorStateList(
                it
            )
        }

        setupEndIcon(END_ICON_CLEAR_TEXT) //always set to clear_text except those that specify it (textinput_areacode e.g)

        isExpandedHintEnabled = false

        setPrefixTextAppearance(R.style.Text2)
        val prefixColor = ContextCompat.getColorStateList(context, R.color.al2_grey)
        prefixColor?.let { setPrefixTextColor(it) }

        prefixTextView.updateLayoutParams {
            height = ViewGroup.LayoutParams.MATCH_PARENT
        }
        prefixTextView.gravity = Gravity.CENTER

        if (attrs != null) {
            val typedArray = context.obtainStyledAttributes(attrs, R.styleable.Al2TextInputLayout)

            // label
            val label = typedArray.getString(R.styleable.Al2TextInputLayout_textLabel)
            if (!label.isNullOrEmpty()) {
                setLabel(label)
            }

            // caption
            val caption = typedArray.getString(R.styleable.Al2TextInputLayout_textCaption)
            if (caption != null) {
                setCaption(caption)
            }

            // link
            val link = typedArray.getString(R.styleable.Al2TextInputLayout_textLink)
            if (link != null) {
                setLink(link)
            }

            // type
            val type = typedArray.getInt(
                R.styleable.Al2TextInputLayout_textInputLayoutType,
                Type.STANDARD.type
            )

            when (type) {
                Type.SEARCH.type -> {
                    isHintEnabled = false
                }
                Type.USER.type -> {
                    // leave empty for now
                }
                Type.PASSWORD.type -> {
                    editText?.transformationMethod = DotPasswordTransformationMethod
                    endIconMode = END_ICON_CUSTOM
                    endIconDrawable = ContextCompat.getDrawable(context, R.drawable.ic_view)
                    endIconDrawable?.setTint(ContextCompat.getColor(context, R.color.al2_grey))
                    setEndIconOnClickListener {
                        if (passwordVisible) {
                            endIconDrawable = ContextCompat.getDrawable(context, R.drawable.ic_view)
                            editText?.transformationMethod = DotPasswordTransformationMethod
                        } else {
                            endIconDrawable = ContextCompat.getDrawable(context, R.drawable.ic_hide)
                            editText?.transformationMethod = null
                        }
                        endIconDrawable?.setTint(ContextCompat.getColor(context, R.color.al2_grey))
                        editText?.text?.length?.let { editText?.setSelection(it) }
                        passwordVisible = !passwordVisible
                    }
                }
            }

            typedArray.recycle()
        }
    }

    fun setupEndIcon(iconMode: Int) {
        endIconMode = iconMode
        if (iconMode != END_ICON_NONE) {
            endIconDrawable = ContextCompat.getDrawable(context, R.drawable.ic_close_small)
            setEndIconTintList(ContextCompat.getColorStateList(context, R.color.al2_grey))
        }
    }

    // placeholder/hint on the top of input field
    fun setLabel(label: String) {
        textLabel?.let {
            removeView(textLabel)
        }

        textLabel = Al2TextView(context)
        textLabel?.isEnabled = viewEnable
        textLabel?.setTextAppearance(R.style.TextEyebrowInput)
        textLabel?.isAllCaps = true
        textLabel?.setTextColor(ContextCompat.getColorStateList(context, R.color.textinput_label))
        textLabel?.text = label
        addView(textLabel, 0)
    }

    // caption text on the bottom of input field
    fun setCaption(
        text: String?,
        captionType: CaptionType? = CaptionType.STANDARD
    ) {

        val lineStroke = when (captionType) {
            CaptionType.SUCCESS -> ContextCompat.getColor(context, R.color.textinput_bottomline_success)
            CaptionType.ERROR -> ContextCompat.getColor(context, R.color.textinput_bottomline_error)
            else -> defaultBoxStrokeColor
        }
        // bottom line stroke for unfocused state
        setDefaultStrokeColor(lineStroke)
        // bottom line stroke for focused state
        boxStrokeColor = lineStroke

        text?.let {
            val colorText: Int = when (captionType) {
                CaptionType.SUCCESS -> R.color.al2_success_green
                CaptionType.ERROR -> R.color.al2_error_red
                else -> R.color.al2_grey
            }

            helperText = text
            helperText.apply {
                setHelperTextTextAppearance(R.style.Text2)

                val captionColor = if (viewEnable) colorText else R.color.al2_light_grey
                setHelperTextColor(ColorStateList.valueOf(ContextCompat.getColor(context, captionColor)))

            }
        }

        textLink?.let {
            it.visibility = if (captionType == CaptionType.STANDARD) VISIBLE else GONE
        }
    }

    private fun setLink(text: String, onTextLinkClicked: OnTextLinkClickListener? = null) {
        textLink?.let {
            removeView(it)
        }

        textLink = Al2TextView(context)
        textLink?.text = text
        textLink?.apply {
            setTextAppearance(R.style.TextLink1)
            paintFlags = paintFlags or Paint.UNDERLINE_TEXT_FLAG
            val color = if (viewEnable) R.color.al2_blue else R.color.al2_light_grey
            setTextColor(ContextCompat.getColor(context, color))
            layoutParams = LayoutParams(
                LayoutParams.MATCH_PARENT,
                LayoutParams.WRAP_CONTENT
            ).apply {
                setMargins(0, 10f.toDp(context), 0, 0)
            }
        }

        onTextLinkClicked?.let { setLinkClickListener(it) }

        addView(textLink)
    }

    fun setLinkEnabled(enabled: Boolean) {
        textLink?.isEnabled = enabled
    }

    fun setLinkClickListener(onTextLinkClicked: OnTextLinkClickListener) {
        onTextLinkClickListener = onTextLinkClicked
        onTextLinkClickListener?.let { onClick ->
            textLink?.setOnClickListener {
                onClick()
            }
        }
    }

    private fun setDefaultStrokeColor(color: Int) {
        try {
            val defaultStrokeColor = TextInputLayout::class.java.getDeclaredField("defaultStrokeColor")
            defaultStrokeColor.isAccessible = true
            defaultStrokeColor.set(this, color)
        } catch (e: NoSuchFieldException) {
            // failed to change the color
        }
    }
}

I tried:

  • Use LinearLayout instead of ConstraintLayout
  • Use minHeight on both TextInputLayout to force equal heights

Source: View source