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

写在前面:本文是实际开发中遇到的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("");
    }
}

亲测有效。

转载 https://www.jianshu.com/p/e2e8dfd92bab

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值