开篇废话
发现项目中At好友功能的类,在某些情况下会有一些bug,所以重新梳理了一下逻辑,重新写了一个可以在EditText中显示At好友,高亮处理,删除整体,并且支持发布之后在TextView上展示,支持超链接点击等功能。
AtUserHelper之GitHub地址,帮我点个Star,赠人玫瑰,手留余香,谢谢。
先讲思路
通过正则来匹配需要解析成一个Spannable
,并将数据存储在Spannable
,可以当做一个整体的At
,在删除时判断是否将要删除一个Spannable
,在发布的时候通过Spannable
拿到解析前的数据,然后转成和服务端约定好的数据格式进行发布。
正则表达式
首先要和其它端商量出一个一起用的正则表达式,我现在项目中使用的正则是这样的:@\(name:([\s\S]*?),id:([A-Za-z0-9]+)\)
解析带At的原始字符串
我们先写如果从服务端已经拿到了带At数据的字符串,如何解析成要展示给用户可以高亮可点击的样式。
主要以下几个步骤:
- 通过正则进行解析。
- 将解析出的字符串替换为用户名。
- 通过添加自定义
ForegroundColorSpan
,改变文字颜色。 - 通过添加
ClickableSpan
,给文字添加点击事件。 - 返回解析好的
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整体
当输入@符号之后,我们将跳到另一个页面,然后点击跳转回来,携带name
和uid
参数,然后将其转化为我们需要的字符串,然后解析。
/**
* 将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整体
删除整体时分以下几个步骤:
- 先通过输入前和输入后的字符串进行判断是删除状态。
- 通过
SpannableStringBuilder
拿到所有我们自定义的ForegroundColorSpan
。 - 循环遍历我们删除是否有包含自定义的
ForegroundColorSpan
。 - 如果有包含则删除。
/**
* @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中只能选择整体
选择整体分以下几个步骤:
- 自定义一个
EditText
,添加选择位置的监听。 - 添加监听使
EditText
在选择时如果选择了自定义ForegroundColorSpan
的部分,刚强制选择整体。
public class SelectionEditText extends AppCompatEditText {
private List 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。
更多内容戳这里(整理好的各种文集)