
TopicEditText
import android.content.Context;
import android.graphics.Color;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.ReplacementTransformationMethod;
import android.text.style.BackgroundColorSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import com.hjq.toast.Toaster;
import com.jrzfveapp.network.api.VideoTagInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TopicEditText extends androidx.appcompat.widget.AppCompatEditText {
private int preTextLength = 0;
private final int mForegroundColor = Color.parseColor("#00F1F5");
private final int mBackgroundColor = Color.parseColor("#EE82EE");
private final String PREFIX_TAG = "#";
private final String END_TAG = " ";
private String tagRegex = DEF_REGEX;
public static final String DEF_REGEX = "#[^#|\\s]+?\\s|#[^#|\\s]+?$";
private boolean isChanged;
private List<String> tagList = new ArrayList<>();
private List<VideoTagInfo> tagInfoList = new ArrayList<>();
private String selectTagStr;
public void setTagInfoList(List<VideoTagInfo> tagInfoList) {
this.tagInfoList = tagInfoList;
}
public List<VideoTagInfo> getTagInfoList() {
return tagInfoList;
}
public TopicEditText(Context context) {
super(context);
initView();
}
public TopicEditText(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public TopicEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
public void insertTopic(VideoTagInfo tagInfo, int maxLength) {
String topicStr = tagInfo.getContent();
if (TextUtils.isEmpty(topicStr)) {
return;
}
topicStr = PREFIX_TAG + topicStr;
int index = getSelectionStart();
Editable editable = getText();
if (topicStr.length() >= maxLength - editable.length()) {
Toaster.show("超出最大长度了~");
return;
}
if (topicStr.length() > 1) {
editable.insert(index, topicStr.trim() + END_TAG);
tagInfoList.add(tagInfo);
} else {
editable.insert(index, topicStr.trim());
}
setSelection(index + topicStr.length() + END_TAG.length());
}
private void initView() {
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable editable) {
if (TextUtils.isEmpty(editable)) {
preTextLength = 0;
tagList.clear();
tagInfoList.clear();
return;
}
if (isChanged) return;
String inputStr = editable.toString().replace("#", PREFIX_TAG);
int length = inputStr.length();
Log.d("caowj", "\n====afterTextChanged()====length:" + length + ",,preTextLength:" + preTextLength);
if (length > preTextLength) {
tagList = new ArrayList<>();
if (TextUtils.isEmpty(tagRegex)) tagRegex = DEF_REGEX;
Pattern pattern = Pattern.compile(tagRegex);
Matcher matcher = pattern.matcher(inputStr);
SpannableStringBuilder builder = new SpannableStringBuilder(inputStr);
while (matcher.find()) {
int pos = matcher.start();
int end = matcher.end();
Log.d("caowj", "匹配开始位置:" + pos + "-->" + end);
String item = matcher.group(0);
SpannableString span = TopicSpan.getSpan(mForegroundColor, item, item, null);
builder = builder.delete(pos, pos + span.length());
builder.insert(pos, span);
tagList.add(item);
}
if (tagList.isEmpty()) {
selectTagStr = "";
return;
}
isChanged = true;
int preIndex = getSelectionStart();
Log.d("caowj", "设置高亮前:" + getSelectionStart() + "-->" + getSelectionEnd());
setText(builder);
Log.d("caowj", "设置高亮后:" + getSelectionStart() + "-->" + getSelectionEnd());
isChanged = false;
setSelection(preIndex);
} else if (preTextLength > length) {
}
preTextLength = length;
}
});
setTransformationMethod(new ReplacementTransformationMethod() {
@Override
protected char[] getOriginal() {
char[] ori = {'#'};
return ori;
}
@Override
protected char[] getReplacement() {
char[] rep = {'#'};
return rep;
}
});
setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_DEL) {
if (tagList.isEmpty()) {
selectTagStr = "";
return false;
}
if (!TextUtils.isEmpty(selectTagStr) && !tagInfoList.isEmpty() && !getText().toString().contains(selectTagStr)) {
Log.d("caowj", "刚刚删除了官方的话题:" + selectTagStr);
for (int i = 0; i < tagInfoList.size(); i++) {
if (selectTagStr.replace(PREFIX_TAG, "").trim().equals(tagInfoList.get(i).getContent())) {
tagInfoList.remove(i);
}
}
}
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
if (selectionStart < selectionEnd) {
Log.d("caowj", "删除已选中的部分");
return false;
}
int lastPos = 0;
for (int i = 0; i < tagList.size(); i++) {
String topicTxt = tagList.get(i);
if (!topicTxt.endsWith(END_TAG)) {
Log.d("caowj", "正常的删除操作");
} else {
lastPos = getText().toString().indexOf(topicTxt, lastPos);
if (lastPos != -1) {
if (selectionStart != 0 && selectionStart > lastPos && selectionStart <= (lastPos + topicTxt.length())) {
Log.d("caowj", "选中了话题:" + topicTxt);
setSelection(lastPos, lastPos + topicTxt.length());
selectTagStr = topicTxt;
getText().setSpan(new BackgroundColorSpan(mBackgroundColor), lastPos, lastPos + topicTxt.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return true;
}
lastPos += topicTxt.length();
}
}
}
}
return false;
}
});
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (selStart != selEnd) {
Log.d("caowj", "选中了范围,无需移到光标");
return;
}
if (tagList == null || tagList.size() == 0) {
return;
}
Log.d("caowj", "======onSelectionChanged...话题数量:" + tagList.size());
Log.d("caowj", "\n======onSelectionChanged====selStart=" + selStart + ",,selEnd=" + selEnd);
int startPosition = 0;
int endPosition;
String topicTxt;
int length = getText().toString().length();
for (int i = 0; i < tagList.size(); i++) {
topicTxt = tagList.get(i);
if (!topicTxt.endsWith(END_TAG)) {
Log.w("caowj", "结尾编辑中的话题");
} else {
while (true) {
startPosition = getText().toString().indexOf(topicTxt, startPosition);
endPosition = startPosition + topicTxt.length();
if (startPosition < 0) {
break;
}
if (selStart > startPosition && selStart <= endPosition) {
Log.d("caowj", "设置光标位置,length=" + length + ",,startPosition=" + startPosition + ",,endPosition=" + endPosition);
setSelection(endPosition);
break;
}
startPosition = endPosition;
}
}
}
}
public List<String> getTagList() {
List list = new ArrayList();
if (TextUtils.isEmpty(tagRegex)) tagRegex = DEF_REGEX;
Editable editable = getText();
Pattern pattern = Pattern.compile(tagRegex);
Matcher matcher = pattern.matcher(editable);
if (editable.toString().contains(PREFIX_TAG)) {
while (matcher.find()) {
list.add(matcher.group(0).replace(PREFIX_TAG, "").replace(END_TAG, ""));
}
}
return list;
}
}
TopicSpan
package com.jrzfveapp.widgets;
import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;
public class TopicSpan<T> extends ClickableSpan {
int defColor = Color.parseColor("#00F1F5");
int color = defColor;
float textSize = -1;
boolean underline = false;
boolean bolder = false;
T data;
Callback<T> callback;
public TopicSpan(int color, float textSize, boolean underline, boolean bolder, T data, Callback<T> callback) {
this.color = color == -1 ? defColor : color;
this.textSize = textSize;
this.underline = underline;
this.bolder = bolder;
this.data = data;
this.callback = callback;
}
@Override
public void onClick(View widget) {
if (callback != null) {
callback.onClick(data);
}
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(color);
if (textSize > -1) {
ds.setTextSize(textSize);
}
ds.setFakeBoldText(bolder);
ds.setUnderlineText(underline);
ds.clearShadowLayer();
}
public static class Builder<T> {
int color = Color.parseColor("#1fabf3");
float textSize = -1;
boolean underline = false;
boolean bolder = false;
T data;
Callback<T> callback;
public Builder(int color) {
this.color = color;
}
public Builder color(int color) {
this.color = color;
return this;
}
public Builder textSize(float textSize) {
this.textSize = textSize;
return this;
}
public Builder underline(boolean underline) {
this.underline = underline;
return this;
}
public Builder data(T data) {
this.data = data;
return this;
}
public Builder bolder(boolean bolder) {
this.bolder = bolder;
return this;
}
public Builder callback(Callback<T> callback) {
this.callback = callback;
return this;
}
public TopicSpan build() {
return new TopicSpan(color, textSize, underline, bolder, data, callback);
}
}
public interface Callback<T> {
void onClick(T data);
}
public static <T> SpannableString getSpan(int color, String displayText, T data, Callback<T> callback) {
SpannableString span = new SpannableString(displayText);
TopicSpan tag = new Builder<T>(color)
.data(data)
.callback(callback).build();
span.setSpan(tag, 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return span;
}
public static <T> SpannableString getSpan(int color, float textSize, String displayText, boolean bolder, T data, Callback<T> callback) {
SpannableString span = new SpannableString(displayText);
TopicSpan tag = new Builder<T>(color)
.data(data)
.bolder(bolder)
.textSize(textSize)
.callback(callback).build();
span.setSpan(tag, 0, span.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return span;
}
}