将EditText添加/移除到TextInputLayout中

4

"我们已经有一个EditText,只能有一个"

我为我的应用程序构建了一个片段(LoginFragment),它处理两种主要的身份验证模式之一,即登录和注册用户。有一个按钮允许用户在“登录模式”和“注册模式”之间切换。每个“模式”都有一些其他模式不需要的额外视图。因此,需要在切换模式时添加和删除视图。

我正在使用TextInputLayout布局中的EditText视图。当我执行以下操作时,我的应用程序会崩溃:

  • 以编程方式添加EditText
  • 以编程方式删除EditText
  • 以编程方式添加EditText -> 崩溃

这是我得到的错误:

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)

这段代码来自于android.support.design.widget.TextInputLayout,它有一个内部私有的EditText变量,当视图被添加时设置(源代码如下)。看起来当我尝试第二次将视图添加到TextInputLayout中时,mEditText变量已经被设置。该类没有自己的.removeView()方法,因此我不知道应该如何删除它?
我怀疑我正在错误地删除EditText视图,但无法弄清楚我错在哪里。我也阅读了一些其他处理删除视图的Stack Overflow帖子,但这些方法也没有解决问题。
以下是一些参考链接:
  • 添加视图 - https://dev59.com/F2Yr5IYBdhLWcg3wLnVA#13889257
  • 删除视图 - 首先调用子项的父级removeView()
  • 有人有什么想法可以让这个工作吗?
    以下是我的代码供参考。

    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(来自最新的22.2.1支持库)

    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;
            }
        }
    }
    
    2个回答

    3

    在 com.android.support.design 库(v22.2.1)中存在一个限制。您无法直接在运行时从 TextInputLayout 中删除并添加 EditText。您可以在此处收藏此错误

    我为此问题设计了一个解决方案。我修改了xml布局,这样我们就不需要在运行时添加/删除EditText视图到TextInputLayout(这样做是无效的),而是将TextInputLayout本身添加/删除到LinearLayout容器中。通过这个解决方案,我们永远不需要真正地从TextInputLayout中删除EditText。

    唯一需要注意的是,这个解决方案会使您的视图层次结构比它实际需要的深1级。因此,如果您已经遇到UI性能问题,请记住这一点。当您阅读此内容时,如果已经提供了 com.android.support.design v22.2.1 的版本,可以检查是否已解决此问题。

    否则,请参考下面的示例代码来实现我的解决方案。

    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>
    

    0

    恭喜你,你(也许?)发现了一个可能的 bug(或者说是在移除 TextInputLayoutEditText 时出现了非预期的行为?)

    你可以看到,removeView() 是 ViewGroup 的一个方法。它会从ViewGroup 的子视图数组中移除你的 View,但不会移除 InputTextLayoutEditText 的引用。

    那我该怎么办呢?

    你需要扩展 TextInputLayout 并创建自己的方法,将 super.mEditText 设置为 null。问题是,你仍然需要调用这两个方法,因为仅仅将一个 Layout 引用设置为 null 可能会导致旧布局在应用程序生命周期中一直存在于内存中。


    我之前尝试过这个方法,但是mEditText被声明为私有的,这样做会违反面向对象编程的封装规则。我应该在原始帖子的代码片段中包含声明,现在已经包含了。 - Maurice Gavin
    1
    嗯,我没想到会这样。也许可以扩展LinearLayout,复制整个TextInputLayout的源代码并声明一个setEditText()方法? - VulfCompressor
    @MauriceGavin 有结果了吗? - VulfCompressor
    我为这个问题提交了一个错误报告:https://code.google.com/p/android/issues/detail?id=181574 - Maurice Gavin

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接