開發聊天功能,須要在羣聊中實現@xxx功能,網上沒有找到現成的東西能夠直接拿來用的,那就本身擼一個好了react
項目地址 github.com/sunhapper/S…
用法說明 SpEditTool使用指南
歡迎star,提PR、issuegit
原本不想使用繼承這樣侵入的方式去實現,可是須要監聽光標的變化,而sdk並無提供設置光標監聽的方法。github
這個是實現功能的關鍵點,總結了下網上的方案正則表達式
MentionEditTextbash
這個庫中使用了正則表達式去匹配字符串中的特殊字符串,並且必須嚴格的@開頭空格結尾,這種方式對於特殊字符串中間帶@或者空格的的狀況沒法處理,對只想把@視爲普通字符的狀況也沒法處理數據結構
RichEditorapp
這個庫本身維護了一個List,記錄了特殊字符串的內容,在刪除或者光標變化時遍歷這個List判斷光標是否處在特殊字符串的位置 最初本身咋一看以爲能夠知足需求,在List的元素中加一個字段就能夠記錄@xxx的數據結構了,可是簡單用了以後發現一個很嚴重的問題:像@11 @1這樣前面是相同內容的字符串處理的時候遍歷算出的位置是不對的,並且很容易觸發setSelection的遞歸調用致使StackOverflowide
SpEditToolpost
本身寫的庫,容我自賣自詡一下 這裏利用了Spannable的setSpan方法爲對應的特殊字符串設置一個Object做爲標記,好處有這麼兩點ui
主要代碼:
/**
* 插入特殊字符串,提供給外部調用
* @param showContent 特殊字符串顯示在文本框中的內容
* @param rollBack 是否往前刪除一個字符,由於@的時候可能留了一個字符在輸入框裏
* @param customData 特殊字符串的數據結構
* @param customSpan 特殊字符串的樣式
*/
public void insertSpecialStr(String showContent, boolean rollBack, Object customData,
Object customSpan) {
if (TextUtils.isEmpty(showContent)) {
return;
}
int index = getSelectionStart();
Editable editable = getText();
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(editable);
//SpData中保存了顯示內容和對應數據結構
SpData spData = new SpData();
spData.setShowContent(showContent);
spData.setCustomData(customData);
SpannableString spannableString = new SpannableString(showContent);
spannableString
.setSpan(spData, 0, spannableString.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
//設置自定義樣式
if (customSpan != null) {
spannableString
.setSpan(customSpan, 0, spannableString.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
}
//是否回刪一個字符
if (rollBack) {
spannableStringBuilder.delete(index - 1, index);
index--;
}
spannableStringBuilder.insert(index, spannableString);
setText(spannableStringBuilder);
//將光標置到插入內容末尾
setSelection(index + spannableString.length());
}
複製代碼
使用Spanned接口的getSpans方法
public SpData[] getSpDatas() {
Editable editable = getText();
SpData[] spanneds = editable.getSpans(0, getText().length(), SpData.class);
if (spanneds != null && spanneds.length > 0) {
for (SpData spData : spanneds) {
int start = editable.getSpanStart(spData);
int end = editable.getSpanEnd(spData);
//設置當前特殊字符串的起止位置
spData.setEnd(end);
spData.setStart(start);
}
sortSpans(editable, spanneds, 0, spanneds.length - 1);//獲取到的數據多是沒排過序的,因此快排排個序再返回
return spanneds;
} else {
return new SpData[]{};
}
}
複製代碼
覆蓋onSelectionChanged方法
/**
* 監聽光標位置,對插入的特殊字符一塊兒刪除
*/
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
SpData[] spDatas = getSpDatas();
for (int i = 0; i < spDatas.length; i++) {
SpData spData = spDatas[i];
int startPostion = spData.start;
int endPostion = spData.end;
if (changeSelection(selStart, selEnd, startPostion, endPostion, false)) {
return;
}
}
}
複製代碼
使用EditText的setOnKeyListener,監聽刪除事件,若是碰到特殊字符串總體刪除
setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DEL && event.getAction() == KeyEvent.ACTION_DOWN) {
return onDeleteEvent();
}
return false;
}
});
複製代碼
private boolean onDeleteEvent() {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
if (selectionEnd!=selectionStart){
return false;
}
SpData[] spDatas = getSpDatas();
for (int i = 0; i < spDatas.length; i++) {
SpData spData = spDatas[i];
int rangeStart = spData.start;
if (selectionStart == spData.end) {
getEditableText().delete(rangeStart, selectionEnd);
return true;
}
}
return false;
}
複製代碼
EditText能夠添加一個TextWatcher監聽文本的變化(並非必要的,能夠本身在外部處理)
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
//reactKeys是須要響應的字符列表,不單單能夠響應@
for (Character character : reactKeys) {
if (count == 1 && !TextUtils.isEmpty(charSequence)) {
char mentionChar = charSequence.toString().charAt(start);
if (character.equals(mentionChar) && mKeyReactListener != null) {
handKeyReactEvent(character);//在EditText內部,因此用回調的方式通知外部有特殊的字符被輸入
return;
}
}
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
複製代碼
private void handKeyReactEvent(final Character character) {
post(new Runnable() {
@Override
public void run() {
mKeyReactListener.onKeyReact(character.toString());
}
});
}
複製代碼
在onTextChanged
中使用post(Runnable runnabe)
去調用外部回調,是由於在onTextChanged執行時,最初插入的@等字符的onSelectionChanged
回調還沒走
假設輸入了@
,不使用post(Runnable runnabe)
,直接調用onKeyReact
,在回調中插入@sunhapper
字符串並設置光標位置,onSelectionChanged調用順序爲onSelectionChanged(10,10)
-->onSelectionChanged(1,1)
致使光標位置位於插入字符串前面而不是後面,不符合預期
使用post(Runnable runnabe)可讓當前線程的代碼執行完再去調用onKeyReact
,onSelectionChanged調用順序爲onSelectionChanged(1,1)
-->onSelectionChanged(10,10)
,光標位置符合預期
完成以上幾步,一個支持插入@ #話題#等各類要高亮要總體刪除的EditText就完成了
歡迎你們使用已有的輪子
項目地址github.com/sunhapper/S… 歡迎star,提PR、issue