Search code examples
androidandroid-layoutandroid-viewandroid-design-libraryandroid-textinputlayout

Add/remove EditText to/from TextInputLayout


"We already have an EditText, can only have one"

I've built a Fragment for my app (LoginFragment) that handles both 2 of the main authentication modes; namely logging in and signing up a user. There is a button to allow the user to switch between "login mode" and "sign up" mode. Each "mode" has some additional views that aren't required by the other. Therefore it is necessary to add and remove views as the mode is toggled.

I'm using EditText views within TextInputLayout layouts. My application crashes when I do the following:

  • Add the EditText programmatically
  • Remove the EditText programmatically
  • Add the EditText programmatically -> Crash

This is the error that I get is:

java.lang.IllegalArgumentException: We already have an EditText, can only have one
                at android.support.design.widget.TextInputLayout.setEditText(TextInputLayout.java:166)
                at android.support.design.widget.TextInputLayout.addView(TextInputLayout.java:155)
                at android.view.ViewGroup.addView(ViewGroup.java:3985)
                at android.view.ViewGroup.addView(ViewGroup.java:3961)
                at com.mydomain.myapp.fragments.LoginFragment.showActivateAccountViews(LoginFragment.java:317)

This comes from android.support.design.widget.TextInputLayout which has an internal private EditText variable that is set when the view is added (source below). It appears that when I try to add the view to the TextInputLayout for the 2nd time that the mEditText variable has already set. The class does not have it's own .removeView() method so I don't know how it should be removed?

I suspect that I am removing the EditText view incorrectly but cannot figure out what I am doing wrong. I've also read some other Stack Overflow posts that deal with removing views but these approaches have not resolved the issue either.

Does anyone have any ideas on how I can get this to work?

Below is my own code for reference.

LoginFragment.java

...
import android.support.design.widget.TextInputLayout;
import android.widget.EditText;

public class LoginFragment extends Fragment {

    private RelativeLayout mContainer;

    ...

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        final View view = inflater.inflate(R.layout.fragment_login, container, false);
        mContainer = ((RelativeLayout) view.findViewById(R.id.login_container));

        showLoginViews();

        LayoutTransition layoutTransition = mContainer.getLayoutTransition();
        layoutTransition.enableTransitionType(LayoutTransition.CHANGING);

        return view;
    }

    /**
     * Show the view elements for Login mode
     */
    private void showLoginViews() {

        LayoutInflater li = (LayoutInflater)getActivity().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);

        // Configure the button for the primary action
        Button loginButton = (Button)mContainer.findViewById(R.id.button_login_fragment_primary_action);
        ...

        // Configure the toggle button to navigate to Activate Account mode
        TextView toggleButton = (TextView)mContainer.findViewById(R.id.button_toggle_mode);
        toggleButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoginFragment.this.showActivateAccountViews();
            }
        });
        toggleButton.setText(getResources().getString(R.string.action_activate_account));

        // Hide the Member ID EditText
        ((TextInputLayout)mContainer.findViewById(R.id.member_id_inputlayout)).removeView(mContainer.findViewById(R.id.editText_member_id_field));
    }

    /**
     * Show view elements for Activate Account mode
     */
    private void showActivateAccountViews() {
        LayoutInflater li = (LayoutInflater)getActivity().getSystemService(
                Context.LAYOUT_INFLATER_SERVICE);

        // Configure the primary button for the primary action - Activate Account
        Button activateAccountButton = (Button)mContainer.findViewById(R.id.button_login_fragment_primary_action);
        ...

        // Add the Member ID EditText
        ((TextInputLayout)mContainer.findViewById(R.id.member_id_inputlayout)).addView(li.inflate(R.layout.login_member_id_element_layout, (ViewGroup)mContainer.findViewById(R.id.member_id_inputlayout), false));

        // Configure the toggle button to navigate to Login mode
        TextView toggleButton = (TextView)mContainer.findViewById(R.id.button_toggle_mode);
        toggleButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoginFragment.this.showLoginViews();
            }
        });
        toggleButton.setText(getResources().getString(R.string.action_login));
    }

    ...
}

login_member_id_element_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/editText_member_id_field"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/member_id" />

login_fragment.xml

<android.support.design.widget.CoordinatorLayout 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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:context="com.mydomain.myapp.fragments.LoginFragment">

    <RelativeLayout
        android:id="@+id/login_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:animateLayoutChanges="true">

        <!--placeholder layout with params for activate account elements-->
        <android.support.design.widget.TextInputLayout
            android:id="@+id/member_id_inputlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <!-- a view can be added here-->
        </android.support.design.widget.TextInputLayout>

        <android.support.design.widget.TextInputLayout
            android:id="@+id/email_inputlayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <EditText
                android:id="@+id/editText_email_field"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:inputType="textEmailAddress" />

        </android.support.design.widget.TextInputLayout>

        <Button
            android:id="@+id/button_login_fragment_primary_action"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/password_inputlayout"
            android:text="@string/action_login" />

        <!-- Toggle button for Login/Activate Account-->
        <TextView
            android:id="@+id/button_toggle_mode"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/action_activate_account" />

    </RelativeLayout>

</android.support.design.widget.CoordinatorLayout>

android.support.design.widget.TextInputLayout (from the latest 22.2.1 support library)

public class TextInputLayout extends LinearLayout {

    private EditText mEditText;

    ...

    public void addView(View child, int index, LayoutParams params) {
        if(child instanceof EditText) {
            android.widget.LinearLayout.LayoutParams params1 = this.setEditText((EditText)child, params);
            super.addView(child, 0, params1);
        } else {
            super.addView(child, index, params);
        }

    }

    private android.widget.LinearLayout.LayoutParams setEditText(EditText editText, LayoutParams lp) {
        if(this.mEditText != null) {
            throw new IllegalArgumentException("We already have an EditText, can only have one");
        } else {
            this.mEditText = editText;
            this.mCollapsingTextHelper.setExpandedTextSize(this.mEditText.getTextSize());
            this.mEditText.addTextChangedListener(new TextWatcher() {
                public void afterTextChanged(Editable s) {
                    TextInputLayout.this.mHandler.sendEmptyMessage(0);
                }

                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                }

                public void onTextChanged(CharSequence s, int start, int before, int count) {
                }
            });
            this.mDefaultTextColor = this.mEditText.getHintTextColors().getDefaultColor();
            this.mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
                public void onFocusChange(View view, boolean focused) {
                    TextInputLayout.this.mHandler.sendEmptyMessage(0);
                }
            });
            if(TextUtils.isEmpty(this.mHint)) {
                this.setHint(this.mEditText.getHint());
                this.mEditText.setHint((CharSequence)null);
            }

            if(this.mErrorView != null) {
                ViewCompat.setPaddingRelative(this.mErrorView, ViewCompat.getPaddingStart(this.mEditText), 0, ViewCompat.getPaddingEnd(this.mEditText), this.mEditText.getPaddingBottom());
            }

            this.updateLabelVisibility(false);
            android.widget.LinearLayout.LayoutParams newLp = new android.widget.LinearLayout.LayoutParams(lp);
            Paint paint = new Paint();
            paint.setTextSize(this.mCollapsingTextHelper.getExpandedTextSize());
            newLp.topMargin = (int)(-paint.ascent());
            return newLp;
        }
    }
}

Solution

  • There appears to be a limitation in the com.android.support.design library (v22.2.1). You cannot directly remove and then add an EditText to a TextInputLayout at runtime. You can star this bug here.

    I've devised a workaround for the problem. I modified the xml layout so that instead of adding/removing EditText views from the TextInputLayout at runtime (which doesn't work) we add/remove the TextInputLayout itself to a LinearLayout holder. With this solution we never need to actually remove the EditText from the TextInputLayout.

    The only thing to note about this solution is that it makes your view hierarchy 1 level deeper than it otherwise need to be. So bear this in mind if you already have UI performance problems. if when you are reading this and version of com.android.support.design v22.2.1 is available it may be worth checking to see if this issue has been resolved.

    Otherwise see the sample code below for my implementation of the workaround.

    LoginFragment.java

    import android.support.design.widget.TextInputLayout;
    import android.widget.EditText;
    
    public class LoginFragment extends Fragment {
    
        private RelativeLayout mContainer;
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    
            final View view = inflater.inflate(R.layout.fragment_login, container, false);
            mContainer = ((RelativeLayout) view.findViewById(R.id.login_container));
    
            showLoginViews();
    
            LayoutTransition layoutTransition = mContainer.getLayoutTransition();
            layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
    
            return view;
        }
    
        /**
         * Show the view elements for Login mode
         */
        private void showLoginViews() {
    
            LayoutInflater li = (LayoutInflater)getActivity().getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
    
            // Configure the toggle button to navigate to Activate Account mode
    s        TextView toggleButton = (TextView)mContainer.findViewById(R.id.button_toggle_mode);
            toggleButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    LoginFragment.this.showActivateAccountViews();
                }
            });
            toggleButton.setText(getResources().getString(R.string.action_activate_account));
    
            // Hide the Member ID EditText
            ((LinearLayout)mContainer.findViewById(R.id.member_id_holderlayout)).removeView(mContainer.findViewById(R.id.member_id_inputlayout));
        }
    
        /**
         * Show view elements for Activate Account mode
         */
        private void showActivateAccountViews() {
            LayoutInflater li = (LayoutInflater)getActivity().getSystemService(
                    Context.LAYOUT_INFLATER_SERVICE);
    
            // Configure the primary button for the primary action - Activate Account
            Button activateAccountButton = (Button)mContainer.findViewById(R.id.button_login_fragment_primary_action);
    
            // Add the Member ID EditText
            ((LinearLayout)mContainer.findViewById(R.id.member_id_holderlayout)).addView(li.inflate(R.layout.login_member_id_element_layout, (ViewGroup) mContainer.findViewById(R.id.member_id_inputlayout), false));
    
            // Configure the toggle button to navigate to Login mode
            TextView toggleButton = (TextView)mContainer.findViewById(R.id.button_toggle_mode);
            toggleButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    LoginFragment.this.showLoginViews();
                }
            });
            toggleButton.setText(getResources().getString(R.string.action_login));
        }
    }
    

    login_member_id_element_layout.xml

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/member_id_inputlayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <EditText
            android:id="@+id/editText_member_id_field"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/member_id" />
    
    </android.support.design.widget.TextInputLayout>
    

    login_fragment.xml

    <android.support.design.widget.CoordinatorLayout 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"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    
        <RelativeLayout
            android:id="@+id/login_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <!--placeholder for TextInputLayout to be dynamically added at runtime-->
            <LinearLayout
                android:id="@+id/member_id_holderlayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <!-- a login_member_id_element_layout can be dynamically added/removed here at runtime-->
            </LinearLayout>
    
    
            <!--TextInputLayout for static fields, the EditText is not removed at runtime-->
            <android.support.design.widget.TextInputLayout
                android:id="@+id/email_inputlayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/member_id_holderlayout">
    
                <EditText
                    android:id="@+id/editText_email_field"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:drawablePadding="@dimen/edittext_drawable_padding"
                    android:drawableStart="?emailIcon"
                    android:focusable="true"
                    android:hint="Email"
                    android:inputType="textEmailAddress" />
            </android.support.design.widget.TextInputLayout>
    
        </RelativeLayout>
    
    </android.support.design.widget.CoordinatorLayout>