前言
這個(gè)功能看似簡(jiǎn)單,網(wǎng)上搜出來(lái)的都說(shuō)以@+uid+空格這樣的格式處理,但實(shí)際實(shí)現(xiàn)會(huì)發(fā)現(xiàn)有個(gè)問(wèn)題:如果用戶名之間有空格,那么就無(wú)法正確解析出要@的用戶了,而且如果有同名用戶,也無(wú)法區(qū)分。因此若要以這樣簡(jiǎn)單的方式處理,那么對(duì)用戶名就需要一個(gè)復(fù)雜的限制,顯然現(xiàn)在去修改早已定下的規(guī)則是不現(xiàn)實(shí)的。
在segmentfault上找到一個(gè)我認(rèn)為最靠譜的實(shí)現(xiàn)方案,seg上的文章鏈接找不到了,github地址如下:
https://github.com/luckyandyz...
根據(jù)業(yè)務(wù)需求,作了比較大的改動(dòng),大致如下:
只能通過(guò)mentionUser這個(gè)方法增加mention string
簡(jiǎn)化了對(duì)輸入的監(jiān)視
完善了對(duì)range的管理
通過(guò)convertMention的方法,將@uid轉(zhuǎn)換為指定格式的字符串并返回
QQ和微信@人功能對(duì)比QQ:@之后彈出用戶選擇界面,選擇用戶后輸出“@用戶名 ”格式,無(wú)法在@與用戶名之間插入任何字符,刪除時(shí)是整個(gè)刪除
微信:@之后彈出用戶選擇界面,選擇用戶后輸出“@用戶名 ”格式,可以在@與用戶名之間插入字符,刪除時(shí)也是作為整個(gè)刪除
實(shí)現(xiàn)原理:在調(diào)用mentionUser之后,會(huì)在光標(biāo)所在位置插入@username的span,并且創(chuàng)建一個(gè)range保存到arraylist中,該range會(huì)記錄所插入span的起始、終止位置還有插入的用戶信息。
luckyandyzhang的實(shí)現(xiàn)是在每一次textchanged后都會(huì)掃描整個(gè)字符串,生成對(duì)應(yīng)的span。
代碼private final String mMentionTextFormat = "[Mention:%s, %s]"; private Runnable mAction; private int mMentionTextColor; private boolean mIsSelected; private Range mLastSelectedRange; private ArrayList關(guān)于mRangeArrayList; private OnMentionInputListener mOnMentionInputListener; public MentionEditText(Context context) { super(context); init(); } public MentionEditText(Context context, AttributeSet attrs) { super(context, attrs); init(); } public MentionEditText(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { return new HackInputConnection(super.onCreateInputConnection(outAttrs), true, this); } @Override public void setText(final CharSequence text, TextView.BufferType type) { super.setText(text, type); //hack, put the cursor at the end of text after calling setText() method if (mAction == null) { mAction = new Runnable() { @Override public void run() { setSelection(getText().length()); } }; } post(mAction); } @Override protected void onSelectionChanged(int selStart, int selEnd) { super.onSelectionChanged(selStart, selEnd); //avoid infinite recursion after calling setSelection() if (mLastSelectedRange != null && mLastSelectedRange.isEqual(selStart, selEnd)) { return; } //if user cancel a selection of mention string, reset the state of "mIsSelected" Range closestRange = getRangeOfClosestMentionString(selStart, selEnd); if (closestRange != null && closestRange.to == selEnd) { mIsSelected = false; } Range nearbyRange = getRangeOfNearbyMentionString(selStart, selEnd); //if there is no mention string nearby the cursor, just skip if (nearbyRange == null) { return; } //forbid cursor located in the mention string. if (selStart == selEnd) { setSelection(nearbyRange.getAnchorPosition(selStart)); } else { if (selEnd < nearbyRange.to) { setSelection(selStart, nearbyRange.to); } if (selStart > nearbyRange.from) { setSelection(nearbyRange.from, selEnd); } } } /** * set highlight color of mention string * * @param color value from "getResources().getColor()" or "Color.parseColor()" etc. */ public void setMentionTextColor(int color) { mMentionTextColor = color; } /** * 插入mention string * 在調(diào)用該方法前,請(qǐng)先插入一個(gè)字符(如"@"),之后插入的name將會(huì)和該字符組成一個(gè)整體 * @param uid 用戶id * @param name 用戶名字 */ public void mentionUser(int uid, String name) { Editable editable = getText(); int start = getSelectionStart(); int end = start + name.length(); editable.insert(start, name); editable.setSpan(new ForegroundColorSpan(mMentionTextColor), start - 1, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); mRangeArrayList.add(new Range(uid, name, start - 1, end)); } /** * 將所有mention string以指定格式輸出 * @return 以指定格式輸出的字符串 */ public String convertMetionString() { String text = getText().toString(); if (mRangeArrayList.isEmpty()) { return text; } StringBuilder builder = new StringBuilder(""); int lastRangeTo = 0; Collections.sort(mRangeArrayList); for (Range range : mRangeArrayList) { String newChar = String.format(mMentionTextFormat, range.id, range.name); builder.append(text.substring(lastRangeTo, range.from)); builder.append(newChar); lastRangeTo = range.to; } clear(); return builder.toString(); } public void clear() { mRangeArrayList.clear(); setText(""); } /** * set listener for mention character("@") * * @param onMentionInputListener MentionEditText.OnMentionInputListener */ public void setOnMentionInputListener(OnMentionInputListener onMentionInputListener) { mOnMentionInputListener = onMentionInputListener; } private void init() { mRangeArrayList = new ArrayList<>(); mMentionTextColor = Color.RED; //disable suggestion setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); addTextChangedListener(new MentionTextWatcher()); } private Range getRangeOfClosestMentionString(int selStart, int selEnd) { if (mRangeArrayList == null) { return null; } for (Range range : mRangeArrayList) { if (range.contains(selStart, selEnd)) { return range; } } return null; } private Range getRangeOfNearbyMentionString(int selStart, int selEnd) { if (mRangeArrayList == null) { return null; } for (Range range : mRangeArrayList) { if (range.isWrappedBy(selStart, selEnd)) { return range; } } return null; } private class MentionTextWatcher implements TextWatcher { //若從整串string中間插入字符,需要將插入位置后面的range相應(yīng)地挪位 @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { Editable editable = getText(); //在末尾增加就不需要處理了 if (start >= editable.length()) { return; } int end = start + count; int offset = after - count; //清理start 到 start + count之間的span //如果range.from = 0,也會(huì)被getSpans(0,0,ForegroundColorSpan.class)獲取到 if (start != end && !mRangeArrayList.isEmpty()) { ForegroundColorSpan[] spans = editable.getSpans(start, end, ForegroundColorSpan.class); for (ForegroundColorSpan span : spans) { editable.removeSpan(span); } } //清理arraylist中上面已經(jīng)清理掉的range //將end之后的span往后挪offset個(gè)位置 Iterator iterator = mRangeArrayList.iterator(); while (iterator.hasNext()) { Range range = (Range) iterator.next(); if (range.isWrapped(start, end)) { iterator.remove(); continue; } if (range.from >= end) { range.setOffset(offset); } } } @Override public void onTextChanged(CharSequence charSequence, int index, int i1, int count) { if (count == 1 && !TextUtils.isEmpty(charSequence)) { char mentionChar = charSequence.toString().charAt(index); if ("@" == mentionChar && mOnMentionInputListener != null) { mOnMentionInputListener.onMentionCharacterInput(); } } } @Override public void afterTextChanged(Editable editable) { } } //handle the deletion action for mention string, such as "@test" private class HackInputConnection extends InputConnectionWrapper { private EditText editText; private HackInputConnection(InputConnection target, boolean mutable, MentionEditText editText) { super(target, mutable); this.editText = editText; } @Override public boolean sendKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { int selectionStart = editText.getSelectionStart(); int selectionEnd = editText.getSelectionEnd(); Range closestRange = getRangeOfClosestMentionString(selectionStart, selectionEnd); if (closestRange == null) { mIsSelected = false; return super.sendKeyEvent(event); } //if mention string has been selected or the cursor is at the beginning of mention string, just use default action(delete) if (mIsSelected || selectionStart == closestRange.from) { mIsSelected = false; return super.sendKeyEvent(event); } else { //select the mention string mIsSelected = true; mLastSelectedRange = closestRange; setSelection(closestRange.to, closestRange.from); } return true; } return super.sendKeyEvent(event); } @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { if (beforeLength == 1 && afterLength == 0) { return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); } return super.deleteSurroundingText(beforeLength, afterLength); } } //helper class to record the position of mention string in EditText private class Range implements Comparable{ int id; String name; int from; int to; private Range(int id, String name, int from, int to) { this.id = id; this.name = name; this.from = from; this.to = to; } private boolean isWrapped(int start, int end) { return from >= start && to <= end; } private boolean isWrappedBy(int start, int end) { return (start > from && start < to) || (end > from && end < to); } private boolean contains(int start, int end) { return from <= start && to >= end; } private boolean isEqual(int start, int end) { return (from == start && to == end) || (from == end && to == start); } private int getAnchorPosition(int value) { if ((value - from) - (to - value) >= 0) { return to; } else { return from; } } private void setOffset(int offset) { from += offset; to += offset; } @Override public int compareTo(@NonNull Object o) { return from - ((Range)o).from; } } /** * Listener for "@" character */ public interface OnMentionInputListener { /** * call when "@" character is inserted into EditText */ void onMentionCharacterInput(); }
GITHUB
博客
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/65170.html
摘要:系列,自定義實(shí)現(xiàn)知乎首頁(yè)仿今日頭條最強(qiáng)頂部導(dǎo)航指示器,支持種模式系列之一使用打造千變?nèi)f化的指示器優(yōu)雅的為添加和實(shí)現(xiàn)快速滑動(dòng)實(shí)現(xiàn)條目拖拽排序與滑動(dòng)刪除高仿網(wǎng)易新聞首頁(yè)添加,刪除,排序類似大眾點(diǎn)評(píng)美團(tuán)等應(yīng)用的城市選擇器那些酷炫的開(kāi)源庫(kù)整理 Material Design系列,自定義Behavior實(shí)現(xiàn)Android知乎首頁(yè) showImg(http://img.blog.csdn.net/...
閱讀 1869·2021-11-11 11:02
閱讀 1786·2021-09-22 15:55
閱讀 2599·2021-09-22 15:18
閱讀 3551·2019-08-29 11:26
閱讀 3820·2019-08-26 13:43
閱讀 2996·2019-08-26 13:32
閱讀 984·2019-08-26 10:55
閱讀 1027·2019-08-26 10:27