Android @人功能 -- MentionEditText

来源:互联网 发布:mac的qq远程协助在哪里 编辑:程序博客网 时间:2024/06/06 02:32
原文请点击:

Android at人功能 -- MentionEditText


前言

这个功能看似简单,网上搜出来的都说以@+uid+空格这样的格式处理,但实际实现会发现有个问题:如果用户名之间有空格,那么就无法正确解析出要@的用户了,而且如果有同名用户,也无法区分。因此若要以这样简单的方式处理,那么对用户名就需要一个复杂的限制,显然现在去修改早已定下的规则是不现实的。

在segmentfault上找到一个我认为最靠谱的实现方案,seg上的文章链接找不到了,github地址如下:
https://github.com/luckyandyz...

根据业务需求,作了比较大的改动,大致如下:

  • 只能通过mentionUser这个方法增加mention string

  • 简化了对输入的监视

  • 完善了对range的管理

  • 通过convertMention的方法,将@uid转换为指定格式的字符串并返回

QQ和微信@人功能对比

QQ:@之后弹出用户选择界面,选择用户后输出“@用户名 ”格式,无法在@与用户名之间插入任何字符,删除时是整个删除

微信:@之后弹出用户选择界面,选择用户后输出“@用户名 ”格式,可以在@与用户名之间插入字符,删除时也是作为整个删除

实现原理:

在调用mentionUser之后,会在光标所在位置插入@username的span,并且创建一个range保存到arraylist中,该range会记录所插入span的起始、终止位置还有插入的用户信息。

luckyandyzhang的实现是在每一次textchanged后都会扫描整个字符串,生成对应的span。

代码


    private final String mMentionTextFormat = "[Mention:%s, %s]";    private Runnable mAction;    private int mMentionTextColor;    private boolean mIsSelected;    private Range mLastSelectedRange;    private ArrayList<Range> 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     * 在调用该方法前,请先插入一个字符(如'@'),之后插入的name将会和该字符组成一个整体     * @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相应地挪位        @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,也会被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中上面已经清理掉的range            //将end之后的span往后挪offset个位置            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();    }
内容来自网络,仅供学习和参考。



原创粉丝点击