Android EditText @ friends delete the whole block

Opening nonsense

It was found that the class of At friend function in the project will have some bug s in some cases, so we combed the logic again, wrote a new one that can display At friends in EditText, highlight, delete the whole, support display on TextView after publishing, and support hyperlink click and other functions.
GitHub address of AtUserHelper , order me a Star, give me roses and leave fragrance in my hand. Thank you.

Let's talk about ideas first

Regular matching needs to be parsed into a Spannable, and the data is stored in the Spannable, which can be regarded as a whole At. When deleting, judge whether to delete a Spannable. When publishing, get the data before parsing through the Spannable, and then convert it to the data format agreed with the service end for publishing.

regular expression

First, we need to discuss a regular expression with other clients. The regular expressions I use in my project are as follows: @ \ (name:([\s\S]*?),id:([A-Za-z0-9] +) \)

Parse raw string with At

Let's first write how to parse the string with At data from the server into a clickable style to be highlighted for the user.

The main steps are as follows:

  1. Parsing by regular.
  2. Replace the parsed string with the user name.
  3. Change the text color by adding a custom ForegroundColorSpan.
  4. Add click events to the text by adding ClickableSpan.
  5. Return the parsed SpannableStringBuilder.
analytic method
/**
 * @return Resolve AtUser
 */
public static CharSequence parseAtUserLink(CharSequence text, @ColorInt int color, AtUserLinkOnClickListener clickListener) {
    if (TextUtils.isEmpty(text)) {
        return text;
    }

    // Regular matching [text] (link)
    SpannableStringBuilder spannableString = new SpannableStringBuilder(text);

    Matcher matcher = Pattern.compile(AT_PATTERN).matcher(text);
    int replaceOffset = 0; //The offset of the matcher after each replacement
    while (matcher.find()) {
        // Parsing link format is [text] (link)
        final String name = matcher.group(1);
        final String uid = matcher.group(2);

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

        // append the successfully matched string into the result string, and set the click effect
        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);
        }

        //Add hyperlink:
        if (clickListener != null) {
            spannableString.setSpan(new ClickableSpan() {
                @Override
                public void onClick(View v) {
                    //Deselect
                    Spannable spannable = (Spannable) ((TextView) v).getText();
                    Selection.removeSelection(spannable);

                    // Decrypt id
                    String atUserId = uid;
                    if (!TextUtils.isEmpty(uid)) {
                        atUserId = EncryptTool.hashIdsDecode(uid);
                    }
                    //From outside, click to listen:
                    clickListener.onClick(atUserId);
                }

                @Override
                public void updateDrawState(TextPaint ds) {
                    super.updateDrawState(ds);
                    ds.setColor(color);//Set text color
                    ds.setUnderlineText(false);      //Underline settings
                    ds.setFakeBoldText(false);      //Bold setting
                }
            }, clickSpanStart, clickSpanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }

    return spannableString;
}
Custom ForegroundColorSpan
public class AtUserForegroundColorSpan extends ForegroundColorSpan {
    public String name;
    public String uid;
    public String atContent;

    public AtUserForegroundColorSpan(int color) {
        super(color);
    }
}
OnClickListener for callback
public interface AtUserLinkOnClickListener {
    void onClick(String uid);
}

Operate on At in EditText

Here is the usage code.

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) {
            //Determine whether At is entered
            if (AtUserHelper.isInputAt(beforeText.toString(), afterText.toString(), edt.getSelectionEnd())) {
                //The normal code here should jump to @ friends' page, and then add @ content after coming back, so do a delayed operation
                tv.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        AtUserHelper.appendChooseUser(edt, "A programmer with a story", "1234",
                                mTextWatcher, getResources().getColor(R.color.blue));
                    }
                }, 300);
            }

            //Judge whether the At whole is deleted
            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);
        }
    };
Enter the @ symbol in EditText

Judge whether the @ symbol is input, which can be obtained by comparing the strings before and after input.

/**
 * Did you enter At
 */
public static boolean isInputAt(String beforeStr, String afterStr, int editSelectionEnd) {
    if (!TextUtils.isEmpty(afterStr)) {
        if (TextUtils.isEmpty(beforeStr) || afterStr.length() > beforeStr.length()) {//Operation of input content
            if (afterStr.length() >= 1 && editSelectionEnd - 1 >= 0 && (afterStr.subSequence(editSelectionEnd - 1, editSelectionEnd)).equals("@")) {
                return true;
            }
        }
    }
    return false;
}
Add At after entering @ symbol

After entering the @ symbol, we will jump to another page, then click to jump back, carry the name and uid parameters, then convert them into the string we need, and then parse them.

/**
 * After adding User to 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);
        //Splicing rules negotiated with the server
        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);
    }
}
Delete At as a whole

The following steps are taken to delete the whole:

  1. The deletion status is determined by the string before and after input.
  2. Get all our customized ForegroundColorSpan through SpannableStringBuilder.
  3. Loop through whether there is a custom ForegroundColorSpan.
  4. Delete if any.
/**
 * @return Delete AtUser as a whole
 */
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 Delete AtUser as a whole
 */
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()) {//Delete content
        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) {
                //If it is just behind, select it first and click next time to delete it
                editText.setText(beforeStr);
                editText.setSelection(start, end);

                //The second option is to delete directly
//                    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;
}
Only whole can be selected in EditText

The overall selection is divided into the following steps:

  1. Customize an EditText and add listening at the selected location.
  2. Add listening so that if the part of custom ForegroundColorSpan is selected during EditText selection, the whole is just forced to be selected.
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);
    }

}
/**
 * Add selective listening to EditText to make AtUser a whole
 */
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);
                    }
                }
            }
        }
    });
}

Resolve on publish

When publishing, it also needs to be parsed into a unified format with other terminals, that is, the data format when getting the server data, including regular format. Therefore, a method is needed to replace the custom ForegroundColorSpan with regular style.

/**
 * AtUser analysis
 */
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;
}

Closing remarks

Here, the function is fully realized. Here, we mainly apply some API s provided by SpannableStringBuilder, which can facilitate our display of different styles. Just as I used SpannableStringBuilder in my last article TextView by long clicking and selecting.

Enter my CSDN stamp here (my blog navigation)

Tags: Android Android Studio Gradle

Posted on Wed, 17 Nov 2021 01:59:27 -0500 by sazzie