Android some EditText fields don't respond to long-press (API 30+ with layout_width 0dp)

327 Views Asked by At

I'm wondering if anyone else has experienced this strange bug. My layout is complicated but essentially it's rows of view holders with EditText fields.

It worked flawlessly until I updated my target SDK to API 30.

After targetting API 30, some of my EditText fields don't respond to "focus touch events" (the cursor handle doesn't show up, you can't long press, and you can't select any text). You can still type in the field and move the cursor by tapping. Again, some text fields work perfectly fine while others are affected by this bug.

enter image description here

^ The image is not of my app, just to show the cursor handler for understanding.

Both the constraint layout and the linear layout have the same bug.

<!-- in constraint layout -->
<androidx.appcompat.widget.AppCompatEditText
    android:id="@+id/myTextField"
    android:layout_height="wrap_content"
    android:layout_width="0dp"
    app:layout_constraintHorizontal_weight="1"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"/>

<!-- in linear layout -->
<androidx.appcompat.widget.AppCompatEditText
    android:id="@+id/myTextField"
    android:layout_height="wrap_content"
    android:layout_width="0dp"
    android:layout_weight="1"/>

If I change the text fields width to wrap_content it works perfectly for all text fields, but that doesn't fit with my design. I need the text field to fill the available space.

I even tried manually setting the width to a random dp value and it still didn't work.

I've tested on < API 30 devices and it's totally fine.

I've had users report this problem in production as well.

Did I miss something? Why isn't this working on API 30+?

Is there another way to get the EditText field to fill the space?

Update (still not solved):

I've updated my Android Studio to the latest version Arctic Fox 2020.3.1.. I've also updated gradle to 7.0.3, and kotlin to 1.5.31 and all my libraries to the latest version. I'm using androidx.core:core-ktx:1.7.0 and androidx.constraintlayout:constraintlayout:2.1.2.

I've investigated further and found further weirdness. Setting any text or any hint programmatically, causes the bug. Setting the text or hint in xml, totally fine and bug free. The text values are coming from the database, so I need to be able to set them programmatically.

// setting hint or text programmatically in .kt file causes the bug
// any one of the lines below will cause the bug

myTextField.setHint("myTextField hint")
myTextField.hint = "myTextField hint"
myTextField.setText("myTextField text")
myTextField.text = "myTextField text"

// setting hint or text in .xml file does not cause the bug

<androidx.appcompat.widget.AppCompatEditText
    android:id="@+id/myTextField"
    ...
    android:hint="myTextField hint"
    android:text="myTextField text"
    ... />

Update 2 (still not solved):

I opened an issue with Google but they were unable to resolve the issue yet.

https://issuetracker.google.com/issues/209938888

2

There are 2 best solutions below

4
Dankyi Anno Kwaku On

I have tried to replicate your issue. It works fine with targetSdk 30 with updates to your constraints. Please find screenshots below. These are the changes I made.

  1. I updated the constraint properties you were using as they are outdated. Eg. I changed "layout_constraintLeft_toRightOf" to "layout_constraintStart_toStartOf".
  2. The way you declared your constraint properties puts the view out of the visible space of parent. Eg. 'app:layout_constraintLeft_toRightOf="parent"' Puts the leftside of the view to the end of the parent and that makes most of the view invisible.

I updated how you set your constraints based the code you sent in your question. Find the updated code below. You can also look at the screenshots below to see that it works.

        <?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".Activities.MainActivity">

        <!-- THIS IS THE CONSTRAINT LAYOUT-->

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/the_constraintlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintHorizontal_weight="1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/the_lineartlayout">

            <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/myTextField"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:text="ConstraintLayout Sample Text"
                android:gravity="center"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>

            <!--
            <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/myTextField"
                android:layout_height="wrap_content"
                android:layout_width="0dp"
                android:text="This is a sample text"
                android:gravity="center"
                app:layout_constraintHorizontal_weight="1"
                app:layout_constraintLeft_toRightOf="parent"
                app:layout_constraintRight_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>
                -->
        </androidx.constraintlayout.widget.ConstraintLayout>

        <!-- THIS IS THE LINEAR LAYOUT-->
        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/the_lineartlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/the_constraintlayout"
            app:layout_constraintBottom_toBottomOf="parent">
            <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/myTextField2"
                android:layout_height="wrap_content"
                android:text="LinearLayoutCompat Sample Text"
                android:layout_width="0dp"
                android:layout_weight="1"/>
        </androidx.appcompat.widget.LinearLayoutCompat>
    </androidx.constraintlayout.widget.ConstraintLayout>

enter image description here enter image description here enter image description here

0
Lifes On

I never found a fix. Here's a hacky alternate layout solution that worked for my needs and let me keep my design despite the bug. The bug does not happen with this alternate layout (since I use wrap_content for the actual edit text view and that prevents the bug from happening).

design:

[[TEXT FIELD] WRAPPER ]
[    FAKE UNDERLINE   ]

I ended up wrapping my text field in a LinearLayout and stretching that layout to fill the space the text field should have occupied. I made the text field wrap_content and set the background transparent (to maintain the spacing the edit text provides) and created a separate underline view that stretched the entire length of the wrapper. I programmatically redirected touch events from the wrapper to the end of the text field (since if they click the wrapper they're effectively touching the end of the text field).

Note: the magic numbers are just numbers that lined up my fake underline with the original edit text underline. When the original edit text is activated, the underline is a tiny bit bigger and I kept that for my fake underline. Use whatever numbers you like that look nice to you.

xml layout:

<LinearLayout
    android:id="@+id/editTextWrapper"
    android:layout_height="wrap_content"
    android:layout_width="0dp"
    android:layout_weight="1"
    app:layout_constraintHorizontal_weight="1"
    android:orientation="vertical"
    app:layout_constraintStart_toEndOf="@id/whateverView"
    app:layout_constraintEnd_toStartOf="@id/whateverView"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent">

    <androidx.appcompat.widget.AppCompatEditText
        android:id="@+id/editText"
        android:backgroundTint="@android:color/transparent"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:inputType="textMultiLine|textCapSentences"
        android:textSize="16sp"
        android:textColor="?android:textColorPrimary"
        android:textColorHint="?attr/textColorMuted"
        android:fontFamily="sans-serif-condensed"/>

</LinearLayout>

<!-- note: I tried setting this as the background of the editTextWrapper 
     instead of its own view but it didn't work in <= API 21 Lollipop -->
<View
    android:id="@+id/descriptionEditTextUnderline"
    android:background="@drawable/edit_text_underline_bg"
    android:layout_width="0dp"
    android:layout_height="5dp"
    android:layout_marginBottom="-5dp"
    android:translationY="-8.5dp"
    android:layout_marginHorizontal="3.5dp"
    app:layout_constraintStart_toStartOf="@id/editTextWrapper"
    app:layout_constraintEnd_toEndOf="@id/editTextWrapper"
    app:layout_constraintTop_toBottomOf="@id/editTextWrapper"
    app:layout_constraintBottom_toBottomOf="@id/editTextWrapper"/>

edit_text_underline_bg.xml (for lollipop)

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <selector>
            <!-- active state -->
            <item android:state_activated="true">
                <shape android:shape="line">
                    <stroke
                        android:color="?attr/colorControlActivated"
                        android:width="2dp"/>
                </shape>
            </item>
            <!-- regular state -->
            <item>
                <shape android:shape="line">
                    <stroke
                        android:color="?attr/colorControlNormal"
                        android:width="1dp"/>
                </shape>
            </item>
        </selector>
    </item>
</layer-list>

code (kotlin):

editText.setOnFocusChangeListener { view, hasFocus ->
    // manually update the underline state
    syncUnderlineState(hasFocus)
}
// redirect touch events from the wrapper to end of actual text field programmatically
editTextWrapper.setOnTouchListener { _, motionEvent ->
    val updatedMotionEvent = MotionEvent.obtain(
        motionEvent.downTime,
        motionEvent.eventTime,
        motionEvent.action,
        editTextUnderline.width.toFloat(), // change touch event x the end of the description edit text field
        motionEvent.y,
        motionEvent.metaState
    )
    editText.dispatchTouchEvent(updatedMotionEvent)
    true
}

private fun syncUnderlineState(doesEditTextHaveFocus: Boolean) {
    editTextUnderline.isActivated = doesEditTextHaveFocus
    if (doesEditTextHaveFocus) {
        editTextUnderline.translationY = UNDERLINE_FOCUSED_TRANSLATION_Y
        editTextUnderline.updateLayoutParams<ConstraintLayout.LayoutParams> {
            updateMargins(left = UNDERLINE_FOCUSED_MARGIN_HORIZONTAL, right = UNDERLINE_FOCUSED_MARGIN_HORIZONTAL)
        }
    } else {
        editTextUnderline.translationY = UNDERLINE_TRANSLATION_Y
        editTextUnderline.updateLayoutParams<ConstraintLayout.LayoutParams> {
            updateMargins(left = UNDERLINE_MARGIN_HORIZONTAL, right = UNDERLINE_MARGIN_HORIZONTAL)
        }
    }
}

companion object {
    private val UNDERLINE_TRANSLATION_Y = (-8.5F).dpToPixels.toFloat() // keep in sync with task_list_item.xml
    private val UNDERLINE_MARGIN_HORIZONTAL = 3.5F.dpToPixels // keep in sync with task_list_item.xml
    private val UNDERLINE_FOCUSED_TRANSLATION_Y = (-8.25F).dpToPixels.toFloat()
    private val UNDERLINE_FOCUSED_MARGIN_HORIZONTAL = 3F.dpToPixels
}

val Float.dpToPixels get() = (this * Resources.getSystem().displayMetrics.density).toInt()

If you're not supporting API <= 21, you can move the margins and translations from the underline view to the xml drawable and set it as the background of the wrapper.