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)+空格
}
效果如下:
自定義分詞器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)); // 只有分詞符,不額外加空格
}
效果如下:
自定義郵箱域名提示控件
使用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);
}
效果如下: