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

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

查看文章前请参考:Java 中 Emoji 的正则表达式

功能介绍

  1. 过滤换行符,回车符,空白符
  1. 过滤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输入字符的时候字符成倍出现,删除的时候还会输入字符”
我拿来测试机试了一下:

![](https://i.imgur.com/YFPXh4X.jpg)
魅族Note5
![](https://i.imgur.com/IRWXtIg.png)
华为手机

很明显,魅族的输入法会把当前“待输入字符”放入到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. 总结

这个问题暴露的原因主要还是早期覆盖测试不够,但是好在测试同学发现,不然这将是一个线上事故了。
虽然是一个小小的字符问题,但是不管是从技术角度考虑还是客户角度考虑,都要引起足够的重视。
解决过程还是学习到不少东西,比如TransformationMethod可以用来提前做字符变换。我相信这个问题也能用它解决。
出于代码效率和设计考虑,并没有使用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();
	    }
	}
  • 这里两点需要注意的是:

过滤的实质是使用正则表达式完成的,而完整的正则来自于【Java 中 Emoji 的正则表达式】。

绝大部分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工具类)