带有字数统计的 TextInputLayout

作者: __Berial___ | 来源:发表于2015-12-20 11:14 被阅读3760次

    Material Design 官方文档 中的 Errors 一节里,使用的 TextInputLayout 的右下角有一个字数统计的功能,但是我在查看 TextInputLayout 的源码时发现,虽然它在 attrs.xml 里定义了相关属性,在代码中缺没有使用,所以我就把相关的代码都提取出来,单独创建了一个库。
    这是文档中 TextInputLayout 的字数统计效果图:

    未达到字数限制
    达到字数限制

    1.提取相关代码

    首先从 sdk 的 sources 里,把 TextInputLayout 拷出来,根据提示与其相关的类有:


    相关联的类

    从 sdk/extras/android/support/design/libs 中将 android-support-design.jar 拷贝出来,修改拓展名为 .zip 并解压。
    相关资源有:

    attrs.xml :

    <declare-styleable name="TextInputLayout">
        <attr name="hintTextAppearance" format="reference"/>
        <!-- The hint to display in the floating label -->
        <attr name="android:hint"/>
        <!-- Whether the layout is laid out as if an error will be displayed -->
        <attr name="errorEnabled" format="boolean"/>
        <!-- TextAppearance of any error message displayed -->
        <attr name="errorTextAppearance" format="reference"/>
        <!-- Whether the layout is laid out as if the character counter will be displayed -->
        <attr name="counterEnabled" format="boolean"/>
        <!-- The max length to display in the character counter -->
        <attr name="counterMaxLength" format="integer" />
        <!-- TextAppearance of the character counter -->
        <attr name="counterTextAppearance" format="reference"/>
        <!-- TextAppearance of the character counter when the text is longer than the max -->
        <attr name="counterOverflowTextAppearance" format="reference"/>
        <attr name="android:textColorHint"/>
        <!-- Whether to animate hint state changes. -->
        <attr name="hintAnimationEnabled" format="boolean"/>
    </declare-styleable>
    

    style.xml :

    <style name="TextAppearance.Design.Counter" parent="TextAppearance.AppCompat.Caption"/>
    <style name="TextAppearance.Design.Counter.Overflow" parent="TextAppearance.AppCompat.Caption">
        <item name="android:textColor">@color/design_textinput_error_color</item>
    </style>
    <style name="TextAppearance.Design.Error" parent="TextAppearance.AppCompat.Caption">
        <item name="android:textColor">@color/design_textinput_error_color</item>
    </style>
    <style name="TextAppearance.Design.Hint" parent="TextAppearance.AppCompat.Caption">
        <item name="android:textColor">?attr/colorControlActivated</item>
    </style>
    
    <style name="Widget.Design.TextInputLayout" parent="android:Widget">
        <item name="hintTextAppearance">@style/TextAppearance.Design.Hint</item>
        <item name="errorTextAppearance">@style/TextAppearance.Design.Error</item>
        <item name="counterTextAppearance">@style/TextAppearance.Design.Counter</item>
        <item name="counterOverflowTextAppearance">@style/TextAppearance.Design.Counter.Overflow</item>
    </style>
    

    colors.xml:

    <color name="design_textinput_error_color">#FFDD2C00</color>
    

    此时,项目终于不再报错了。

    2. 思考

    根据效果图中计数器的位置,我们可以知道是在与 mErrorView 同一横排的位置,所以在代码中追踪了一下 mErrorView 的创建过程。

    代码中声明了一个 mErrorView 的成员变量,还有两个与其相关的属性。

    private TextView mErrorView;
    private boolean mErrorEnabled; // 是否显示错误提示
    private int mErrorTextAppearance; // 错误提示的文字格式
    

    接下去,我们在构造器中找到了:

    // 获取 xml 里设置的属性值
    mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
    final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
    
    ...
    
    setErrorEnabled(errorEnabled);
    

    我们再看一下 setErrorEnabled 方法:

    public void setErrorEnabled(boolean enabled) {
        if (mErrorEnabled != enabled) {
            if (mErrorView != null) {
                ViewCompat.animate(mErrorView).cancel();
            }
    
            if (enabled) {
                // 创建 mErrorView,并进行相关设置
                mErrorView = new TextView(getContext());
                mErrorView.setTextAppearance(getContext(), mErrorTextAppearance);
                mErrorView.setVisibility(INVISIBLE);
                addView(mErrorView); // 添加到当前 textInputLayout 中
    
                if (mEditText != null) {
                    // Add some start/end padding to the error so that it matches the EditText
                    ViewCompat.setPaddingRelative(mErrorView, ViewCompat.getPaddingStart(mEditText),
                            0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
                }
            } else {
                // 如果设置为不显示错误时,移除 mErrorView
                removeView(mErrorView);
                mErrorView = null;
            }
            mErrorEnabled = enabled;
        }
    }
    

    还有一个有参考价值的方法:

    public void setError(@Nullable CharSequence error) {
        
        ...
    
        if (!TextUtils.isEmpty(error)) {
            
            ... 省略了动画效果设置
    
            // Set the EditText's background tint to the error color
            ViewCompat.setBackgroundTintList(mEditText,
                    ColorStateList.valueOf(mErrorView.getCurrentTextColor()));
        } else {
            if (mErrorView.getVisibility() == VISIBLE) {
                
                ... 省略了动画效果设置
    
                // Restore the 'original' tint, using colorControlNormal and colorControlActivated
                final TintManager tintManager = TintManager.get(getContext());
                ViewCompat.setBackgroundTintList(mEditText,
                        tintManager.getTintList(R.drawable.abc_edit_text_material));
            }
        }
    }
    

    省略了对 mErrorView 显示和隐藏时动画效果的代码,剩下的 ViewCompat.setBackgroundTintList() 方法是对 editText 的那条底线的颜色设置。

    2. 修改代码

    在知道了 mErrorView 的创建流程,正式开始对代码动刀。

    2.1 定义成员变量

    由于 TextInputLayout 继承自 LinearLayout ,为了可以使自己定义的 mCounterView 与 mErrorView 保持在统一横排,且位于控件最右方,我在这里定义了一个 RelativeLayout,并将 mErrorView 也挪到相对布局中。

    private RelativeLayout mBottomBar;
    private TextView mCounterView;
    private boolean mCounterEnabled;
    private int mCounterMaxLength;
    

    2.2 在构造器中添加

    // 是否显示计数器
    final boolean counterEnabled = a.getBoolean(R.styleable.TextInputLayout_counterEnabled, false);
    
    // 最大字数长度限制
    mCounterMaxLength = a.getInt(R.styleable.TextInputLayout_counterMaxLength, 0);
    
    mTooltip = new RelativeLayout(context);
    addView(mTooltip); // 将底部提示条添加到 TextInputLayout 中
    setCounterEnabled(counterEnabled);
    

    2.3 创建 setCounterEnabled 方法

    public void setCounterEnabled(boolean enabled) {
        if (mCounterEnabled != enabled) {
            if (enabled) {
                mCounterView = new TextView(getContext());
                // 根据此时输入的文字的长度对字体格式进行设置,避免与setError产生冲突
                if (mEditText != null && mEditText.length() > mCounterMaxLength) {
                    mCounterView.setTextAppearance(getContext(), mErrorTextAppearance);
                } else {
                    mCounterView.setTextAppearance(getContext(), R.style.TextAppearance_Design_Counter);
                }
                // mCounterView.setVisibility(VISIBLE);
                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                        RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
                params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
                params.addRule(RelativeLayout.CENTER_VERTICAL);
                mBottomBar.addView(mCounterView, params);
    
                if (mEditText != null) {
                    // Add some start/end padding to the counter so that it matches the EditText
                    ViewCompat.setPaddingRelative(mCounterView, ViewCompat.getPaddingStart(mEditText),
                            0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
                } 
                mCounterView.setText(mResources.getString(R.string.counterMaxLength,
                            0, mCounterMaxLength));
            } else {
                mBottomBar.removeView(mCounterView);
                mCounterView = null;
            }
            mCounterEnabled = enabled;
        }
    }
    

    我仿照 setErrorEnabled 在自己创建的 setCounterEnabled 方法中完成了对 mCounterView 的创建,设置布局参数,设置初始文字等操作。

    2.4 更新显示效果

    在 TextInputLayout 中,对起内部的 mEditText 添加了一个 TextWatcher 监听,我在其中添加了 updateCounterView 方法

    mEditText.addTextChangedListener(new TextWatcher() {
            
        @Override
        public void afterTextChanged(Editable s) {
            updateLabelVisibility(true);
            updateCounterText(s);
        }
        ... 
    }
    
    private void updateCounterText(Editable text) {
        if (mCounterView != null) {
            final int currentLength = text.length();
           
            //<string name="counterMaxLength">%1$d/%2$d</string>
            mCounterView.setText(mResources.getString(R.string.counterMaxLength,
                    currentLength, mCounterMaxLength));
            
            // 如果超过最大限制,则将文字和底线变成红色
            if (currentLength == mCounterMaxLength + 1) {
                mCounterView.setTextAppearance(getContext(), mErrorTextAppearance);
                ViewCompat.setBackgroundTintList(mEditText,
                        ColorStateList.valueOf(mResources.getColor(R.color.design_textinput_error_color)));
            } else if (currentLength == mCounterMaxLength) {
                // 当字数从超出限制回到了允许的长度范围,则恢复默认颜色
                mCounterView.setTextAppearance(getContext(), R.style.TextAppearance_Design_Counter);
                // 当不显示 error 信息时,对底线颜色进行修改
                if (!mErrorEnabled) {
                    ViewCompat.setBackgroundTintList(mEditText, mFocusedTextColor);
                }
            }
        }
    }
    

    为了避免重复设置,我这里根据判断临界值来设置当前状态下的显示颜色。

    2.5 修改 setError

    public void setError(@Nullable CharSequence error) {
        
        ...
    
        if (!TextUtils.isEmpty(error)) {
            
            ... 
    
        } else {
            if (mErrorView.getVisibility() == VISIBLE) {
                
                ... 省略了动画效果设置
    
                // 避免与计数器的效果冲突
                if (mEditText.length() > mCounterMaxLength) {
                    return;
                }            
    
                final TintManager tintManager = TintManager.get(getContext());
                ViewCompat.setBackgroundTintList(mEditText,
                        tintManager.getTintList(R.drawable.abc_edit_text_material));
            }
        }
    }
    

    3. 总结

    通过上面的修改,TextInputLayout已经具有了字数统计的功能,下面是效果图:


    效果图

    项目源码

    相关文章

      网友评论

      • neumeng:论技术,我只认B神。
      • 26caeef51d3c:论技术,我只认B神。
      • 一直在变的风:开发工具:idea。libs中我引入了android-support-design,为什么我使用的TextInputLayout,会报错。Error inflating class android.support.design.widget.TextInputLayout
        __Berial___:@锋田鸣歌 在 sdk/extras/android/m2repository/com/android/support/design 目录下,有你下载过的所有版本,里面可以找到对应的aar包,复制到libs下应该就可以了
        一直在变的风: @__Berial___ 谢谢,对我只考了jar包,所以出错了,idea用design太麻烦了,
        。还好最后用工程依赖解决了。
        __Berial___:@锋田鸣歌 你是往libs里复制了design的jar包?因为jar包内不包含resource资源,所以会报错,如果想用,需要在gradle里配置依赖(compile 'com.android.support:design:latestVersion'),或者依赖design的aar包。
      • 捡淑:马克
      • Brioal:很赞
        __Berial___:@1a9535b7ff8d 多谢, :joy:
      • 明镜本清净anany:毕竟B神, :wink:
        __Berial___:@兰公子 :joy:
      • 苏村的南哥:赞一个,感谢__Berial___! 效果很炫,mark. :smirk:
        __Berial___:@KnightOneAdmin :joy:

      本文标题:带有字数统计的 TextInputLayout

      本文链接:https://www.haomeiwen.com/subject/vazphttx.html