带有字数统计的 TextInputLayout

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已经具有了字数统计的功能,下面是效果图:


效果图

项目源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,463评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • 以前呢,公司的保安换得特别频繁,因为实在是太闲了,公司招保安,不过是为了提防那些打着拖欠农民工工资的旗号,来坑蒙拐...
    o沙鸥o阅读 338评论 0 0
  • 昨天一个前辈跟我说“多写才会写”,上午看了sinkcup的这篇和后续几篇东西,脚本代码能看懂一些,自信心一下子有了...
    度京阅读 1,010评论 3 1
  • 听说有这样一个理论。 在手里拿着锤子的人看来,所有的东西都会是钉子。 讲的是思维定式,就是说任何工具带来便利的同时...
    王健波阅读 340评论 0 1