AndroidAndroid开发经验谈Android开发

Android EditText @好友 删除整块

2021-06-30  本文已影响0人  一个有故事的程序员

开篇废话

发现项目中At好友功能的类,在某些情况下会有一些bug,所以重新梳理了一下逻辑,重新写了一个可以在EditText中显示At好友,高亮处理,删除整体,并且支持发布之后在TextView上展示,支持超链接点击等功能。
AtUserHelper之GitHub地址,帮我点个Star,赠人玫瑰,手留余香,谢谢。

先讲思路

通过正则来匹配需要解析成一个Spannable,并将数据存储在Spannable,可以当做一个整体的At,在删除时判断是否将要删除一个Spannable,在发布的时候通过Spannable拿到解析前的数据,然后转成和服务端约定好的数据格式进行发布。

正则表达式

首先要和其它端商量出一个一起用的正则表达式,我现在项目中使用的正则是这样的:@\(name:([\s\S]*?),id:([A-Za-z0-9]+)\)

解析带At的原始字符串

我们先写如果从服务端已经拿到了带At数据的字符串,如何解析成要展示给用户可以高亮可点击的样式。

主要以下几个步骤:

  1. 通过正则进行解析。
  2. 将解析出的字符串替换为用户名。
  3. 通过添加自定义ForegroundColorSpan,改变文字颜色。
  4. 通过添加ClickableSpan,给文字添加点击事件。
  5. 返回解析好的SpannableStringBuilder
解析方法
/**
 * @return 解析AtUser
 */
public static CharSequence parseAtUserLink(CharSequence text, @ColorInt int color, AtUserLinkOnClickListener clickListener) {
    if (TextUtils.isEmpty(text)) {
        return text;
    }

    // 进行正则匹配[文字](链接)
    SpannableStringBuilder spannableString = new SpannableStringBuilder(text);

    Matcher matcher = Pattern.compile(AT_PATTERN).matcher(text);
    int replaceOffset = 0; //每次替换之后matcher的偏移量
    while (matcher.find()) {
        // 解析链接  格式是[文字](链接)
        final String name = matcher.group(1);
        final String uid = matcher.group(2);

        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(uid)) {
            continue;
        }

        // 把匹配成功的串append进结果串中, 并设置点击效果
        String atName = "@" + name + "";
        int clickSpanStart = matcher.start() - replaceOffset;
        int clickSpanEnd = clickSpanStart + atName.length();
        spannableString.replace(matcher.start() - replaceOffset, matcher.end() - replaceOffset, atName);
        replaceOffset += matcher.end() - matcher.start() - atName.length();

        if (color != 0) {
            AtUserForegroundColorSpan atUserLinkSpan = new AtUserForegroundColorSpan(color);
            atUserLinkSpan.name = name;
            atUserLinkSpan.uid = uid;
            atUserLinkSpan.atContent = matcher.group();
            spannableString.setSpan(atUserLinkSpan, clickSpanStart, clickSpanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        //是否加超链接:
        if (clickListener != null) {
            spannableString.setSpan(new ClickableSpan() {
                @Override
                public void onClick(View v) {
                    //取消选择
                    Spannable spannable = (Spannable) ((TextView) v).getText();
                    Selection.removeSelection(spannable);

                    // 对id进行解密
                    String atUserId = uid;
                    if (!TextUtils.isEmpty(uid)) {
                        atUserId = EncryptTool.hashIdsDecode(uid);
                    }
                    //外面传进来点击监听:
                    clickListener.onClick(atUserId);
                }

                @Override
                public void updateDrawState(TextPaint ds) {
                    super.updateDrawState(ds);
                    ds.setColor(color);//设置文字颜色
                    ds.setUnderlineText(false);      //下划线设置
                    ds.setFakeBoldText(false);      //加粗设置
                }
            }, clickSpanStart, clickSpanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }

    return spannableString;
}
自定义的ForegroundColorSpan
public class AtUserForegroundColorSpan extends ForegroundColorSpan {
    public String name;
    public String uid;
    public String atContent;

    public AtUserForegroundColorSpan(int color) {
        super(color);
    }
}
回调的OnClickListener
public interface AtUserLinkOnClickListener {
    void onClick(String uid);
}

对EditText中的At进行操作

这里是使用代码。

private void initView() {
        edt.addTextChangedListener(mTextWatcher);
}
private TextWatcher mTextWatcher = new TextWatcher() {
        private int beforeEditStart;
        private int beforeEditEnd;
        private SpannableStringBuilder beforeText, afterText;

        public void afterTextChanged(Editable s) {
            //判断是否输入了At
            if (AtUserHelper.isInputAt(beforeText.toString(), afterText.toString(), edt.getSelectionEnd())) {
                //这里正常的代码应该是跳到@好友的页面,然后回来之后做添加@内容,所以做个延迟的操作
                tv.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        AtUserHelper.appendChooseUser(edt, "一个有故事的程序员", "1234",
                                mTextWatcher, getResources().getColor(R.color.blue));
                    }
                }, 300);
            }

            //判断是否删除了At整体
            AtUserHelper.isRemoveAt(edt, mTextWatcher, beforeText, afterText, s, beforeEditStart, beforeEditEnd);
        }

        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            beforeText = new SpannableStringBuilder(s);
            beforeEditStart = edt.getSelectionStart();
            beforeEditEnd = edt.getSelectionEnd();
        }

        public void onTextChanged(CharSequence s, int start, int before, int count) {
            afterText = new SpannableStringBuilder(s);
        }
    };
在EditText是否输入@符号

判断是否输入了@符号,可以通过输入前和输入之后的字符串对比获得。

/**
 * 是否输入了At
 */
public static boolean isInputAt(String beforeStr, String afterStr, int editSelectionEnd) {
    if (!TextUtils.isEmpty(afterStr)) {
        if (TextUtils.isEmpty(beforeStr) || afterStr.length() > beforeStr.length()) {//输入内容的操作
            if (afterStr.length() >= 1 && editSelectionEnd - 1 >= 0 && (afterStr.subSequence(editSelectionEnd - 1, editSelectionEnd)).equals("@")) {
                return true;
            }
        }
    }
    return false;
}
输入@符号之后添加At整体

当输入@符号之后,我们将跳到另一个页面,然后点击跳转回来,携带nameuid参数,然后将其转化为我们需要的字符串,然后解析。

/**
 * 将User添加到At之后
 */
public static void appendChooseUser(EditText editText, String name, String uid, TextWatcher watcher, @ColorInt int color) {
    if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(uid)) {
        editText.removeTextChangedListener(watcher);
        //@(name:xxxxx,id:XOVo9x)
        String atUserId = EncryptTool.hashIdsEncode(uid);
        //和服务端商量好的拼接规则
        String result = "@(name:" + name + ",id:" + atUserId + ")";
        int beforeTextLength = editText.length();
        int selectionEnd = editText.getSelectionEnd();
        editText.getText().replace(selectionEnd - 1, selectionEnd, result);
        editText.setText(parseAtUserLink(editText.getText(), color));
        int afterTextLength = editText.length();
        editText.setSelection(afterTextLength - beforeTextLength + selectionEnd);
        editText.addTextChangedListener(watcher);
    }
}
删除At整体

删除整体时分以下几个步骤:

  1. 先通过输入前和输入后的字符串进行判断是删除状态。
  2. 通过SpannableStringBuilder拿到所有我们自定义的ForegroundColorSpan
  3. 循环遍历我们删除是否有包含自定义的ForegroundColorSpan
  4. 如果有包含则删除。
/**
 * @return 是否删除AtUser整体
 */
public static boolean isRemoveAt(EditText editText, TextWatcher watcher,
                                    CharSequence beforeStr, CharSequence afterStr, Editable s,
                                    int editSelectionStart, int editSelectionEnd) {
    editText.removeTextChangedListener(watcher);
    boolean isRemove = isRemoveAt(editText, beforeStr, afterStr, s, editSelectionStart, editSelectionEnd);
    editText.addTextChangedListener(watcher);
    return isRemove;
}

/**
 * @return 是否删除AtUser整体
 */
public static boolean isRemoveAt(EditText editText,
                                 CharSequence beforeStr, CharSequence afterStr, Editable s,
                                 int editSelectionStart, int editSelectionEnd){
    if (TextUtils.isEmpty(afterStr) || TextUtils.isEmpty(beforeStr)
            || !(afterStr instanceof SpannableStringBuilder)
            || !(beforeStr instanceof SpannableStringBuilder)) {
        return false;
    }
    if (afterStr.length() < beforeStr.length()) {//删除内容的操作
        SpannableStringBuilder beforeSp = (SpannableStringBuilder) beforeStr;
        AtUserForegroundColorSpan[] beforeSpans = beforeSp.getSpans(0, beforeSp.length(), AtUserForegroundColorSpan.class);
        boolean mReturn = false;
        for (AtUserForegroundColorSpan span : beforeSpans) {
            int start = beforeSp.getSpanStart(span);
            int end = beforeSp.getSpanEnd(span);

            boolean isRemove = false;
            if (editSelectionStart == editSelectionEnd && editSelectionEnd == end) {
                //如果刚后在后面,先选中,下次点击才删除
                editText.setText(beforeStr);
                editText.setSelection(start, end);

                //方案二是直接删除
//                    isRemove = true;
//                    s.delete(start, end - 1);
            } else if (editSelectionStart <= start && editSelectionEnd >= end) {
                return false;
            } else if (editSelectionStart <= start && editSelectionEnd > start) {
                isRemove = true;
                s.delete(editSelectionStart, end - editSelectionEnd);
            } else if (editSelectionStart < end && editSelectionEnd >= end) {
                isRemove = true;
                s.delete(start, editSelectionStart);
            }

            if (isRemove) {
                mReturn = true;
                beforeSp.removeSpan(span);
            }
        }
        return mReturn;
    }
    return false;
}
在EditText中只能选择整体

选择整体分以下几个步骤:

  1. 自定义一个EditText,添加选择位置的监听。
  2. 添加监听使EditText在选择时如果选择了自定义ForegroundColorSpan的部分,刚强制选择整体。
public class SelectionEditText extends AppCompatEditText {

    private List<OnSelectionChangeListener> onSelectionChangeListeners;

    private OnSelectionChangeListener onSelectionChangeListener;

    public SelectionEditText(Context context) {
        super(context);
    }

    public SelectionEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SelectionEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onSelectionChanged(int selStart, int selEnd) {
        super.onSelectionChanged(selStart, selEnd);
        if (onSelectionChangeListener != null) {
            onSelectionChangeListener.onSelectionChange(selStart, selEnd);
        }
        if (onSelectionChangeListeners != null) {
            for (int i = 0; i < onSelectionChangeListeners.size(); i++) {
                onSelectionChangeListeners.get(i).onSelectionChange(selStart, selEnd);
            }
        }
    }

    public void addOnSelectionChangeListener(OnSelectionChangeListener onSelectionChangeListener) {
        if (onSelectionChangeListeners == null) {
            onSelectionChangeListeners = new ArrayList<>();
        }
        onSelectionChangeListeners.add(onSelectionChangeListener);
    }

    public void removeOnSelectionChangedListener(OnSelectionChangeListener onSelectionChangeListener) {
        if (onSelectionChangeListeners != null) {
            onSelectionChangeListeners.remove(onSelectionChangeListener);
        }
    }

    public void clearOnSelectionChangedListener() {
        if (onSelectionChangeListeners != null) {
            onSelectionChangeListeners.clear();
        }
    }

    public void setOnSelectionChangeListener(OnSelectionChangeListener onSelectionChangeListener) {
        this.onSelectionChangeListener = onSelectionChangeListener;
    }

    public interface OnSelectionChangeListener {
        void onSelectionChange(int selStart, int selEnd);
    }

}
/**
 * 给EditText添加选择监听,使AtUser成为一个整体
 */
public static void addSelectionChangeListener(SelectionEditText editText) {
    editText.addOnSelectionChangeListener(new SelectionEditText.OnSelectionChangeListener() {
        @Override
        public void onSelectionChange(int selStart, int selEnd) {
            Editable editable = editText.getText();
            if (editable instanceof SpannableStringBuilder) {
                SpannableStringBuilder spanStr = (SpannableStringBuilder) editable;
                AtUserForegroundColorSpan[] beforeSpans = spanStr.getSpans(0, spanStr.length(), AtUserForegroundColorSpan.class);
                for (AtUserForegroundColorSpan span : beforeSpans) {
                    int start = spanStr.getSpanStart(span);
                    int end = spanStr.getSpanEnd(span);

                    boolean isChange = false;
                    if (selStart > start && selStart < end) {
                        selStart = start;
                        isChange = true;
                    }
                    if (selEnd < end && selEnd > start) {
                        selEnd = end;
                        isChange = true;
                    }

                    if (isChange) {
                        editText.setSelection(selStart, selEnd);
                    }
                }
            }
        }
    });
}

发布时解析

发布的时候还需要将其解析为和其它端统一的格式,也就是拿到服务端数据时的数据格式,包含正则的格式,所以需要一个方法去将自定义的ForegroundColorSpan替换为正则样式。

/**
 * AtUser解析
 */
public static Editable toAtUser(final Editable editable) {
    if (TextUtils.isEmpty(editable)) {
        return null;
    }
    Editable result = editable;
    if (editable instanceof SpannableStringBuilder) {
        SpannableStringBuilder spanStr = (SpannableStringBuilder) editable;
        AtUserForegroundColorSpan[] beforeSpans = spanStr.getSpans(0, spanStr.length(), AtUserForegroundColorSpan.class);
        for (AtUserForegroundColorSpan span : beforeSpans) {
            int start = spanStr.getSpanStart(span);
            int end = spanStr.getSpanEnd(span);
            result.replace(start, end, span.atContent);
        }
    }
    return result;
}

结束小语

到这里功能就完全实现了,这里主要是应用了SpannableStringBuilder提供的一些API,可以方便我们不同样式的展示,就像我上一篇文章TextView长按选择,也一样用到了SpannableStringBuilder。

更多内容戳这里(整理好的各种文集)

上一篇下一篇

猜你喜欢

热点阅读