Android EditText @好友 删除整块

作者: 一个有故事的程序员 | 来源:发表于2021-06-30 17:45 被阅读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。

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

    相关文章

      网友评论

        本文标题:Android EditText @好友 删除整块

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