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:
- Parsing by regular.
- Replace the parsed string with the user name.
- Change the text color by adding a custom ForegroundColorSpan.
- Add click events to the text by adding ClickableSpan.
- 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:
- The deletion status is determined by the string before and after input.
- Get all our customized ForegroundColorSpan through SpannableStringBuilder.
- Loop through whether there is a custom ForegroundColorSpan.
- 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:
- Customize an EditText and add listening at the selected location.
- 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.