自定义Android DevAndroid 自定义控件

带有字数统计的 TextInputLayout

2015-12-20  本文已影响3760人  __Berial___

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


效果图

项目源码

上一篇下一篇

猜你喜欢

热点阅读