使用TextView,如果在代码里添加了maxLength属性,那么有一点需要注意:


如果TextView的文案在显示时发生了截断,那么这时调用TextView.getText()方法返回的字符串,是截断后的子串,而不是之前TextView.setText()方法传入的字符串!


也就是说,使用了maxLength属性后,不能保证setText()和getText()方法的字符串守恒定律。




原因都在代码里,先去看TextView.getText()源码:


public CharSequence getText() {
        return mText;
    }

很简洁,那就去追查mText被赋值的地方吧!于是来到TextView.setText()方法:

private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        if (text == null) {
            text = "";
        }

        // If suggestions are not enabled, remove the suggestion spans from the text
        if (!isSuggestionsEnabled()) {
            text = removeSuggestionSpans(text);
        }
        ......
        int n = mFilters.length;
        for (int i = 0; i < n; i++) {
            CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
            if (out != null) {
                text = out;
            }
        }
        ...... 
        if (type == BufferType.EDITABLE || getKeyListener() != null ||
                needEditableForNotification) {
            createEditorIfNeeded();
            Editable t = mEditableFactory.newEditable(text);
            text = t;
            setFilters(t, mFilters);
            InputMethodManager imm = InputMethodManager.peekInstance();
            if (imm != null) imm.restartInput(this);
        } else if (type == BufferType.SPANNABLE || mMovement != null) {
            text = mSpannableFactory.newSpannable(text);
        } else if (!(text instanceof CharWrapper)) {
            text = TextUtils.stringOrSpannedString(text);
        }

        if (mAutoLinkMask != 0) {
            Spannable s2;

            if (type == BufferType.EDITABLE || text instanceof Spannable) {
                s2 = (Spannable) text;
            } else {
                s2 = mSpannableFactory.newSpannable(text);
            }
             ......
            if (Linkify.addLinks(s2, mAutoLinkMask)) {
                text = s2;
                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;

                /*
                 * We must go ahead and set the text before changing the
                 * movement method, because setMovementMethod() may call
                 * setText() again to try to upgrade the buffer type.
                 */
                mText = text;

                // Do not change the movement method for text that support text selection as it
                // would prevent an arbitrary cursor displacement.
                if (mLinksClickable && !textCanBeSelected()) {
                    setMovementMethod(LinkMovementMethod.getInstance());
                }
            }
        }

        mBufferType = type;
        mText = text;                                                                                                                                             ......                                                                                                                                              }

上面的方法重载了TextView其他几个public的方法,其他的public方法最终都是调用此方法,我省略了一些和mText赋值无关的代码片段(剩下的还是很多)。可以看到这个方法的大致逻辑是根据mBufferType来生成不同的对象(均实现CharSequence接口),例如String、Spanned、Editable或是CharWrapper,并赋值给mText。


除了上述的主逻辑,修改mText变量的地方就剩下setText()中应用filter的代码块了:

int n = mFilters.length;
        for (int i = 0; i < n; i++) {
            CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
            if (out != null) {
                text = out;
            }
        }

由于我当时并没有设置任何过滤器,所以一开始根本没有怀疑到这里,直到我不小心看到TextView.setFilters()方法的注释:

/**
     * Sets the list of input filters that will be used if the buffer is
     * Editable. Has no effect otherwise.
     *
     * @attr ref android.R.styleable#TextView_maxLength
     */
    public void setFilters(InputFilter[] filters) {
        if (filters == null) {
            throw new IllegalArgumentException();
        }

        mFilters = filters;

        if (mText instanceof Editable) {
            setFilters((Editable) mText, filters);
        }
    }

没错,这个方法对应的resource 属性居然就是看似无辜的maxLength,赶快去看TextView构造器中读取并应用maxLength的代码:

if (maxlength >= 0) {
            setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
        } else {
            setFilters(NO_FILTERS);
        }

真相大白,添加maxLength属性,就相当于设置了InputFilter,超过maxLength的直接被截断,根本不会保存。



思考:maxLength属性一般给开发者的感觉和layout_width类似,是一个用于约束UI控件布局的属性,但是真没想到它却是会直接修改mText本身的。不知是谷歌设计的瑕疵,还是我自己的思维定势,不过看来以后还是要多读代码,不可望文生义。




有趣的是,当时碰到这个问题在SO上搜时,发现好多人在求问如何让getText()方法返回截断后的子串,而不是返回原始字符串(可以通过TextUtils.ellipsize()方法),当时很诧异:真是身在福中不知福啊!后来才发现,逆行的是自己,哈哈。