MultiAutoCompleteTextView

MultiAutoCompleteTextView通過(guò)分詞器Tokenizer,可以支持連續(xù)提示。即第一次點(diǎn)擊提示信息后,會(huì)自動(dòng)在后面添加分隔符(默認(rèn)為逗號(hào),并加上空格),然后又可以繼續(xù)顯示提示信息。

使用默認(rèn)逗號(hào)分詞器CommaTokenizer

MultiAutoCompleteTextView的使用與AutoCompleteTextView差不多,使用創(chuàng)建并設(shè)置ArrayAdapter數(shù)據(jù)適配器,然后設(shè)置觸發(fā)提示的最少輸入字符數(shù),最后需要設(shè)置定義在其內(nèi)部中的默認(rèn)分詞器CommaTokenizer即可。

private void setupCommaTokenizerTextView() {
    String[] mOnePieceCnArray = getResources().getStringArray(R.array.onepiece_cn);
    ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, mOnePieceCnArray);
    mCommaTokenizerTv = (MultiAutoCompleteTextView) findViewById(R.id.tv_comma);
    mCommaTokenizerTv.setAdapter(adapter);      // 設(shè)置數(shù)據(jù)適配器
    mCommaTokenizerTv.setThreshold(1);          // 設(shè)置觸發(fā)提示閾值
    mCommaTokenizerTv.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());  // 設(shè)置分詞器,默認(rèn)為逗號(hào)+空格
}

效果如下:

comma_tokenizer.gif

自定義分詞器ShellTokenizer

首先CommaTokenizer是定義在MultiAutoCompleteTextView中的內(nèi)部public類,實(shí)現(xiàn)了Tokenizer接口中的三個(gè)方法:
findTokenStart() - 用于查找當(dāng)前光標(biāo)位置之前的分隔符的位置并返回
findTokenEnd() - 用于查找當(dāng)前光標(biāo)位置之后的分隔符的位置并返回
terminateToken() - 用于返回提示信息加上分隔符后的文本內(nèi)容

CommaTokenizer源代碼注釋分析如下:

public static class CommaTokenizer implements Tokenizer {
    // 用于查找當(dāng)前光標(biāo)位置之前的分隔符的位置并返回,向前查詢 
    // text - 用戶已經(jīng)輸入的文本內(nèi)容
    // cursor - 當(dāng)前光標(biāo)的位置,在文本內(nèi)容后面
    public int findTokenStart(CharSequence text, int cursor) {
        int i = cursor;

        // 查找當(dāng)前光標(biāo)的前一個(gè)位置非','的字符位置
        while (i > 0 && text.charAt(i - 1) != ',') {
            i--;
        }
        // 查找','后面非空格的字符位置
        while (i < cursor && text.charAt(i) == ' ') {
            i++;
        }

        return i;   // 返回一個(gè)要加分隔符的字符串的開始位置
    }

    // 用于查找當(dāng)前光標(biāo)位置之后的分隔符的位置并返回,向后查詢
    // text - 用戶已經(jīng)輸入的文本內(nèi)容
    // cursor - 當(dāng)前光標(biāo)的位置,在文本內(nèi)容之間
    public int findTokenEnd(CharSequence text, int cursor) {
        int i = cursor;
        int len = text.length();

        // 向后查找','字符,若找到則直接返回其所在位置
        while (i < len) {       
            if (text.charAt(i) == ',') {    
                return i;
            } else {
                i++;
            }
        }

        return len;
    }

    // 用于返回提示信息加上分隔符后的文本內(nèi)容
    // text - 提示信息中的文本內(nèi)容
    public CharSequence terminateToken(CharSequence text) {
        int i = text.length();

        // 去掉原始匹配的數(shù)據(jù)的末尾空格
        while (i > 0 && text.charAt(i - 1) == ' ') {
            i--;
        }

        // // 判斷原始匹配的數(shù)據(jù)去掉末尾空格后是否含有逗號(hào),有則立即返回
        if (i > 0 && text.charAt(i - 1) == ',') {
            return text;
        } else {
            // CharSequence類型的數(shù)據(jù)有可能是富文本SpannableString類型
            // 故需要進(jìn)行判斷
            if (text instanceof Spanned) {
                // 創(chuàng)建一個(gè)新的SpannableString,傳進(jìn)來(lái)的text會(huì)被退化成String,
                // 導(dǎo)致sp中丟失掉了text中的樣式配置
                SpannableString sp = new SpannableString(text + ", ");
                // 故需要借助TextUtils.copySpansFrom從text中復(fù)制原來(lái)的樣式到新的sp中,
                // 以保持原先樣式不變情況下添加一個(gè)逗號(hào)和空格
                TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
                                        Object.class, sp, 0);
                return sp;
            } else {        // text為純文本,直接加上逗號(hào)和空格
                return text + ", ";
            }
        }
    }
}

通過(guò)以上的CommaTokenizer源碼分析,下面就可以自定義一個(gè)分詞器ShellTokenizer,可以外部指定分隔符,源碼如下:

// This simple Tokenizer can be used for lists where the items are
// separated by a hyphen.
public class ShellTokenizer implements MultiAutoCompleteTextView.Tokenizer {

    private Context context;
    private static int count;
    private char tokenChar;
    private boolean isOnlyToken;

    public ShellTokenizer(Context context) {
        this(context, ',', false);
    }

    public ShellTokenizer(Context context, char tokenChar, boolean isOnlyToken) {
        this.context = context;
        this.tokenChar = tokenChar;
        this.isOnlyToken = isOnlyToken;
        count = 0;
    }

    // Returns the start of the token that ends at offset cursor within text.
    // 5 findTokenStart: Italy, ger, 10     // 每當(dāng)輸入一個(gè)字符后就會(huì)調(diào)用5次
    // findTokenStart: Italy, I, 8, 7       //
    // findTokenStart: Italy, It, 9, 7       //
    @Override
    public int findTokenStart(CharSequence text, int cursor) {
        int i = cursor;

        while (i > 0 && text.charAt(i - 1) != tokenChar) {  // 測(cè)試當(dāng)前光標(biāo)的前一個(gè)位置非','的字符位置
            i--;
        }
        while (i < cursor && text.charAt(i) == ' ') {       // 測(cè)試','后面非空格的字符位置
            i++;
        }

        count++;
        Toast.makeText(context, count + " findTokenStart: " + text + ", " + cursor + ", " + i, Toast.LENGTH_SHORT).show();
        return i;       // 返回一個(gè)要加分隔符的字符串的開始位置
    }

    // Returns the end of the token (minus trailing punctuation)
    // that begins at offset cursor within text.
    @Override
    public int findTokenEnd(CharSequence text, int cursor) {
        Toast.makeText(context, "findTokenEnd: " + text + ", " + cursor, Toast.LENGTH_SHORT).show();
        int i = cursor;
        int len = text.length();

        while (i < len) {
            if (text.charAt(i) == tokenChar) {
                return i;
            } else {
                i++;
            }
        }

        return len;
    }

    // Returns text, modified, if necessary, to ensure that
    // it ends with a token terminator (for example a space or comma).
    // terminateToken: Italy    // 輸入I點(diǎn)擊提示信息Italy時(shí)被調(diào)用,在這之前會(huì)調(diào)用一次findTokenStart()
    // 回調(diào)terminateToken()方法時(shí),傳入的text是原始的數(shù)據(jù) (CharSequence類型的數(shù)據(jù)有可能是富文本SpannableString)
    @Override
    public CharSequence terminateToken(CharSequence text) {
        Toast.makeText(context, "terminateToken: " + text, Toast.LENGTH_SHORT).show();
        int i = text.length();

        while (i > 0 && text.charAt(i - 1) == ' ') {    // 去掉原始匹配的數(shù)據(jù)的末尾空格
            i--;
        }

        if (i > 0 && text.charAt(i - 1) == tokenChar) {       // 判斷原始匹配的數(shù)據(jù)去掉末尾空格后是否含有逗號(hào),有則立即返回
            Toast.makeText(context, "direct return: " + text, Toast.LENGTH_SHORT).show();
            return text;
        } else {

            String result = text + String.valueOf(tokenChar);
            if (!isOnlyToken) {
                result += " ";
            }

            if (text instanceof Spanned) {      // 富文本
                Toast.makeText(context, "Rich Text", Toast.LENGTH_SHORT).show();

                // 創(chuàng)建一個(gè)新的SpannableString,傳進(jìn)來(lái)的text會(huì)被退化成String,導(dǎo)致sp中丟失掉了text中的樣式配置
                SpannableString sp = new SpannableString(result);

                // 故需要借助TextUtils.copySpansFrom從text中復(fù)制原來(lái)的樣式到新的sp中,以保持原先樣式不變情況下添加一個(gè)逗號(hào)和空格
                TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0);

                return sp;
            } else {
                Toast.makeText(context, "Plain Text", Toast.LENGTH_SHORT).show();   // Plaint Text +
                return result;     // 66Italy, 66
            }
        }
    }
}

自定義分詞器ShellTokenizer的使用:

private void setupShellTokenizerTextView() {
    String[] mOnePieceArray = getResources().getStringArray(R.array.onepiece);
    ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, mOnePieceArray);
    mShellTokenizerTv = (MultiAutoCompleteTextView) findViewById(R.id.tv_shell);
    mShellTokenizerTv.setAdapter(adapter);
    mShellTokenizerTv.setThreshold(1);
    mShellTokenizerTv.setTokenizer(new ShellTokenizer(this, '-', true));    // 只有分詞符,不額外加空格
}

效果如下:

shell_tokenizer.gif

自定義郵箱域名提示控件

使用MultiAutoCompleteTextView在指定字符后產(chǎn)生提示信息列表功能來(lái)自定義一個(gè)郵箱域名提示的控件MailBoxAutoCompleteTextView,其直接繼承自MultiAutoCompleteTextView,只要重寫enoughToFilter()方法即可。

// 執(zhí)行流程猜想:
// 0. 每當(dāng)用戶輸入一個(gè)字符或者刪除一個(gè)字符時(shí)都會(huì)觸發(fā)enoughToFilter()方法,進(jìn)行判斷是否包含'@'字符且不是第一個(gè)字符位置
// 1. 如當(dāng)用戶輸入最后一個(gè)字符為'@'即"hello@"時(shí),enoughToFilter()方法測(cè)試成功返回true,將觸發(fā)Tokenizer.findTokenStart()方法,
//    返回當(dāng)前光標(biāo)所在位置,故得到的過(guò)濾后的提示信息將為全部的數(shù)據(jù)信息
// 2. 當(dāng)用戶再次輸入如"hello@1"時(shí),enoughToFilter()方法測(cè)試成功返回true,將觸發(fā)Tokenizer.findTokenStart()方法,
//    '@'后面的字符串的開始位置,故得到的過(guò)濾后的提示信息將為以"1"開頭的數(shù)據(jù)信息 (hello@1, 7, 6)
// 3. 當(dāng)用戶點(diǎn)擊提示信息"163.com"時(shí),會(huì)調(diào)用Tokenizer.findTokenStart()方法得到 (hello@1, 7, 6) 6的開始位置,
//    然后將以6開始的后面的字符串替換成"163.com",完成郵箱后綴的自定補(bǔ)全
public class MailBoxAutoCompleteTextView extends MultiAutoCompleteTextView {

    private Context context;

    public MailBoxAutoCompleteTextView(Context context) {
        this(context, null);
    }

    public MailBoxAutoCompleteTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }

    // 當(dāng)輸入@符號(hào)時(shí),就會(huì)去調(diào)用Tokenizer.findTokenStart()方法一次
    // 當(dāng)點(diǎn)擊下拉提示框中的某個(gè)信息時(shí),會(huì)再次調(diào)用Tokenizer.findTokenStart()方法一次,然后再調(diào)用terminateToken()方法一次
    @Override
    public boolean enoughToFilter() {
        String text = getText().toString();
        // 若用戶輸入的文本字符串中包含'@'字符且不在第一位,則滿足條件返回true,否則返回false
        return text.contains("@") && text.indexOf("@") > 0;
    }
}

自定義用于郵箱域名提示功能的分詞器MailBoxTokenizer

public class MailBoxTokenizer implements MultiAutoCompleteTextView.Tokenizer {
    public int findTokenStart(CharSequence text, int cursor) {
        int i = cursor;

        while (i > 0 && text.charAt(i - 1) != '@') {
            i--;
        }
        return i;
    }

    public int findTokenEnd(CharSequence text, int cursor) {
        int i = cursor;
        int len = text.length();

        while (i < len) {
            if (text.charAt(i) == '@') {
                return i;
            } else {
                i++;
            }
        }

        return len;
    }
    
    // 由于郵箱注冊(cè)信息一般為純文本內(nèi)容,這里就不再進(jìn)行富文本處理,直接返回
    public CharSequence terminateToken(CharSequence text) {
        return text;
    }
}

自定義郵箱域名提示控件MailBoxAutoCompleteTextView的使用:

<com.shellever.multiautocompletetextview.MailBoxAutoCompleteTextView
    android:id="@+id/tv_mail_box"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/text_hint" />
private void setupMailboxTokenizerTextView() {
    String[] mMailboxPostfixArray = getResources().getStringArray(R.array.mail_box_postfix);
    ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, mMailboxPostfixArray);
    mMailboxTokenizerTv = (MultiAutoCompleteTextView) findViewById(R.id.tv_mail_box);
    mMailboxTokenizerTv.setAdapter(adapter);
    mMailboxTokenizerTv.setThreshold(1);    // 因重寫enoughToFilter()方法,故threshold設(shè)置已沒(méi)有意義
    mMailboxTokenizerTv.setDropDownHeight(dp2px(200));     // 設(shè)置自動(dòng)提示列表的高度為200dp
    mMailboxTokenizerTv.setTokenizer(new MailBoxTokenizer());
}

private int dp2px(int dip){
    DisplayMetrics dm = getResources().getDisplayMetrics();
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, dm);
}

效果如下:

mail_box_tokenizer.png
mail_box_tokenizer.gif

源碼參考

MultiAutoCompleteTextView

Links

android自定義本地郵箱聯(lián)想組件(基于MultiAutoCompleteTextView)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,923評(píng)論 18 139
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,241評(píng)論 4 61
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,288評(píng)論 25 708
  • Swift版本點(diǎn)擊這里歡迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh閱讀 25,550評(píng)論 7 249
  • 每一次相遇都是偶然,可每一次分手都是必然,你像夢(mèng)中的雨來(lái)的時(shí)候輕輕悠悠,走的時(shí)候漸漸遠(yuǎn)去,一縷青煙般配著朦朧的月光...
    文心若蘭閱讀 315評(píng)論 0 0