Android at人功能 -- MentionEditText

2399877538 8年前發布 | 7K 次閱讀 安卓開發 Android開發 移動開發

前言

這個功能看似簡單,網上搜出來的都說以@+uid+空格這樣的格式處理,但實際實現會發現有個問題:如果用戶名之間有空格,那么就無法正確解析出要@的用戶了,而且如果有同名用戶,也無法區分。因此若要以這樣簡單的方式處理,那么對用戶名就需要一個復雜的限制,顯然現在去修改早已定下的規則是不現實的。

根據業務需求,作了比較大的改動,大致如下:

  • 只能通過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();
}</code></pre> 

 

來自:https://segmentfault.com/a/1190000007195099

 

 本文由用戶 2399877538 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!