美文网首页Android
Android EditText过滤换行符,回车符和空白符,以及

Android EditText过滤换行符,回车符和空白符,以及

作者: 费麭 | 来源:发表于2018-02-09 18:34 被阅读0次

    写在前面:本文是实际开发中遇到的EditText坑点,记为笔记

    1. 过滤换行符,回车符,空白符
    2. 过滤Emoji
    1. 背景

    项目有个需求,所有与“标题”有关的输入,都不允许有换行。
    第一次拿到这个需求的时候觉得很简单,直接设置一个InputFilter

    public class NewlineFilter implements InputFilter {
    
        /**
         * @param source 输入的文字
         * @param start  输入-0,删除-0
         * @param end    输入-文字的长度,删除-0
         * @param dest   原先显示的内容
         * @param dstart 输入-原光标位置,删除-光标删除结束位置
         * @param dend   输入-原光标位置,删除-光标删除开始位置
         * @return null表示原始输入,""表示不接受输入,其他字符串表示变化值
         */
        @Override
        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
            if (source.toString().contains("\n")) {
                return source.toString().replace("\n", "");
            }
            return null;
        }
    }
    

    然后拿起手机测试,发现没毛病,开开心心的提测了,打算回家过个好年。

    2. 问题

    第二天打开jira一看,有个bug:

    “魅蓝Note5输入字符的时候字符成倍出现,删除的时候还会输入字符”

    我拿来测试机试了一下:


    魅族Note5 华为Mate10

    很明显,魅族的输入法会把当前“待输入字符”放入到EditText输入框里,而华为的讯飞输入法不会。
    再回看上面的代码就会发现一个问题:
    return source.toString().replace("\n", "");会把当前魅族Note5输入框中的“待输入字符”转化为输入字符,但是,推荐词区域的字符并没有丢失,所以下次输入字符的时候会把推荐词内容一并倒入到输入框里,这就是测试同学说的现象。完美复现!

    3. 方案

    好,现在问题明了了,说白就是适配问题。
    解决适配问题有个准则:

    1. 尽量少些特有平台代码
    2. 覆盖测试

    所以我的思考方向是:看看官方怎么实现的

    查阅官方文档,想起了TextView的singleLine,先跑了一遍,发现不论内部输入和外部粘贴,它都直接转化成了空格。这立马勾起了我的兴趣,查看源码,发现有一个很有趣的类TransformationMethod。这个类有点类似于MovementMethod。前者处理字符串变换,后者处理span之类的变换。

    TransformationMethod有个子类:

    /**
     * This transformation method causes the characters in the {@link #getOriginal}
     * array to be replaced by the corresponding characters in the
     * {@link #getReplacement} array.
     */
    public abstract class ReplacementTransformationMethod implements TransformationMethod {
        /**
         * Returns the list of characters that are to be replaced by other
         * characters when displayed.
         */
        protected abstract char[] getOriginal();
        /**
         * Returns a parallel array of replacement characters for the ones
         * that are to be replaced.
         */
        protected abstract char[] getReplacement();
        ...
    }
    

    它有个子类:SingleLineTransformationMethod,TextView的singleLine就是靠这个东西实现的。
    所以我使用了一下,发现效果不错,没有适配问题。不过有个小问题,其实我的需求里是想要把换行直接pass的,看了一下这几个类,没法实现我的需求。
    ReplacementTransformationMethod有另外一个子类:HideReturnsTransformationMethod

    /**
     * This transformation method causes any carriage return characters (\r)
     * to be hidden by displaying them as zero-width non-breaking space
     * characters (\uFEFF).
     */
    public class HideReturnsTransformationMethod
    extends ReplacementTransformationMethod {
        private static char[] ORIGINAL = new char[] { '\r' };
        private static char[] REPLACEMENT = new char[] { '\uFEFF' };
    
        /**
         * The character to be replaced is \r.
         */
        protected char[] getOriginal() {
            return ORIGINAL;
        }
    
        /**
         * The character that \r is replaced with is \uFEFF.
         */
        protected char[] getReplacement() {
            return REPLACEMENT;
        }
    }
    

    他把回车符(回车符是\r,换行符是\n)换成了'\uFEFF',我测了一下这个字符是一个不可见字符,我立马把\n也替换成这个字符,高兴之余,发现这个字符虽然不可见,但是还是占用一个字符位。

    所以我只能找别的方案。
    再回去查看EditText的源码,对于输入内容Editable,它的实现类是SpannableStringBuilder,所以在仔细回想魅族输入法的时候,发现输入的过程中有个小细节:“待输入字符”有下划线,经测试,这些字符是一个span,它标识着自己是“待输入字符”。回想起之前最早的实现,实际上是破坏了这个span,通过查看系统里的InputFilter实现,发现这些实现都是new了一个新的SpannableStringBuilder,同时没有破坏原先的字符串。我照葫芦画瓢,写了一个InputFilter:

    public class CharFilter implements InputFilter {
    
        private final char[] filterChars;
    
        public static CharFilter newlineCharFilter() {
            return new CharFilter(new char[]{'\n'});
        }
    
        public static CharFilter whitespaceCharFilter() {
            return new CharFilter(new char[]{' '});
        }
    
        public static CharFilter returnCharFilter() {
            return new CharFilter(new char[]{'\r'});
        }
    
        public static CharFilter wnrCharFilter() {
            return new CharFilter(new char[]{' ', '\n', '\r'});
        }
    
        private CharFilter(char[] filterChars) {
            this.filterChars = filterChars == null ? new char[0] : filterChars;
        }
    
        /**
         * @param source 输入的文字
         * @param start  输入-0,删除-0
         * @param end    输入-文字的长度,删除-0
         * @param dest   原先显示的内容
         * @param dstart 输入-原光标位置,删除-光标删除结束位置
         * @param dend   输入-原光标位置,删除-光标删除开始位置
         * @return null表示原始输入,""表示不接受输入,其他字符串表示变化值
         */
        @Override
        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
    
            if (needFilter(source)) {
                SpannableStringBuilder builder = new SpannableStringBuilder();
                int abStart = start;
                for (int i = start; i < end; i++) {
                    if (isFilterChar(source.charAt(i))) {
                        if (i != abStart) {
                            builder.append(source.subSequence(abStart, i));
                        }
                        abStart = i + 1;
                    }
                }
    
                if (abStart < end) {
                    builder.append(source.subSequence(abStart, end));
                }
    
                return builder;
            }
    
            return null;
        }
    
        private boolean needFilter(CharSequence source) {
            String s = source.toString();
            for (char filterChar : filterChars) {
                if (s.indexOf(filterChar) >= 0) {
                    return true;
                }
            }
            return false;
        }
    
        private boolean isFilterChar(char c) {
            for (char filterChar : filterChars) {
                if (filterChar == c) {
                    return true;
                }
            }
            return false;
        }
    }
    

    实现非常简单,把之前原字符串里的\n \r 和空格都过滤掉了,剩下的子串按顺序组成新的SpannableStringBuilder

    我覆盖测试后,这个完美的解决了问题。
    这个类有些局限,假如我想过滤所有中文,在魅族Note5上还是会有同样的问题。这个问题有别的解决方案,不在这里阐述。

    4. 总结
    1. 这个问题暴露的原因主要还是早期覆盖测试不够,但是好在测试同学发现,不然这将是一个线上事故了。
    2. 虽然是一个小小的字符问题,但是不管是从技术角度考虑还是客户角度考虑,都要引起足够的重视。
    3. 解决过程还是学习到不少东西,比如TransformationMethod可以用来提前做字符变换。我相信这个问题也能用它解决。
    4. 出于代码效率和设计考虑,并没有使用TextWatcher和自定义EditText。优先考虑解耦的实现方式。
    5. 持续更新

    有一天产品出了一个需求:部分标题类输入不能有特殊字符,比如 各种显示特殊字符和Emoji。

    我立马想到用上述的方法实现:

    package com.icourt.alpha.widget.filter;
    
    import android.text.InputFilter;
    import android.text.SpannableStringBuilder;
    import android.text.Spanned;
    import android.text.TextUtils;
    
    import java.util.regex.Pattern;
    
    /**
     * Description emoji过滤器
     * Company Beijing iCourt.cc
     */
    public class EmojiFilter implements InputFilter {
    
        public static final Pattern EMOJI_PATTERN = Pattern.compile("(?:[\uD83C\uDF00-\uD83D\uDDFF]|[\uD83E\uDD00-\uD83E\uDDFF]|[\uD83D\uDE00-\uD83D\uDE4F]|[\uD83D\uDE80-\uD83D\uDEFF]|[\u2600-\u26FF]\uFE0F?|[\u2700-\u27BF]\uFE0F?|\u24C2\uFE0F?|[\uD83C\uDDE6-\uD83C\uDDFF]{1,2}|[\uD83C\uDD70\uD83C\uDD71\uD83C\uDD7E\uD83C\uDD7F\uD83C\uDD8E\uD83C\uDD91-\uD83C\uDD9A]\uFE0F?|[\u0023\u002A\u0030-\u0039]\uFE0F?\u20E3|[\u2194-\u2199\u21A9-\u21AA]\uFE0F?|[\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55]\uFE0F?|[\u2934\u2935]\uFE0F?|[\u3030\u303D]\uFE0F?|[\u3297\u3299]\uFE0F?|[\uD83C\uDE01\uD83C\uDE02\uD83C\uDE1A\uD83C\uDE2F\uD83C\uDE32-\uD83C\uDE3A\uD83C\uDE50\uD83C\uDE51]\uFE0F?|[\u203C\u2049]\uFE0F?|[\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE]\uFE0F?|[\u00A9\u00AE]\uFE0F?|[\u2122\u2139]\uFE0F?|\uD83C\uDC04\uFE0F?|\uD83C\uDCCF\uFE0F?|[\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA]\uFE0F?)", 
                Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);
    
        @Override
        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
    
            if (needFilter(source)) {
                SpannableStringBuilder builder = new SpannableStringBuilder();
                int abStart = start;
                for (int i = start; i < end; i++) {
                    if (isEmoji(String.valueOf(source.charAt(i)))) {
                        if (i != abStart) {
                            builder.append(source.subSequence(abStart, i));
                        }
                        abStart = i + 1;
                    } else {
                        // 所有的emoji不是一个字符就是两个字符,所以单独处理
                        if (i + 1 <= end && isEmoji(source.subSequence(i, i + 2))) {
                            if (i != abStart) {
                                builder.append(source.subSequence(abStart, i));
                            }
                            abStart = i + 2;
                            i += 1;  // 纠正角标
                        }
                    }
                }
    
                if (abStart < end) {
                    builder.append(source.subSequence(abStart, end));
                }
                return builder;
            }
            return source;
        }
    
        private boolean needFilter(CharSequence source) {
            return EMOJI_PATTERN.matcher(source).find();
        }
    
        private boolean isEmoji(CharSequence str) {
            return EMOJI_PATTERN.matcher(str).match();
        }
    }
    

    这里两点需要注意的是:

    1. 过滤的实质是使用正则表达式完成的,而完整的正则来自于【Java 中 Emoji 的正则表达式】。
    2. 绝大部分emoji都是占用两个字节符,所以对比之前的换行符做了特殊处理。

    所以对于过滤emoji和空白符,换行符以及回车符就非常好办了:
    方案1: 使用上面的两个filter👆
    方案2: 继承于EmojiFilter👇

    public class NameFilter extends EmojiFilter {
    
        private static final String PATTERN_STR = "[\n|\t]";
        private static final Pattern PATTERN = Pattern.compile(PATTERN_STR, Pattern.CASE_INSENSITIVE);
    
        public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
            // 将/n/t替换掉,这里不会出现奇怪的连带效果,亲测有效
            return PATTERN.matcher(super.filter(source, start, end, dest, dstart, dend)).replaceAll("");
        }
    }
    

    亲测有效。


    顺便给公司打个广告 (点击链接查看岗位描述,加微信细聊)

    要想走的快,一个人走;要想走的远,一群人走

    新橙科技

    相关文章

      网友评论

        本文标题:Android EditText过滤换行符,回车符和空白符,以及

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