在做登录模块 Demo 的时候,发现官方模板代码中有个不认识的东西 AutoCompleteTextView,遂查阅官方文档学习下它的用法。
AutoCompleteTextView 的基本用法是根据用户输入的文字内容给出可能的补全方案,用户选择后补全内容会自动体会原有输入。
AutoCompleteTextView 实际上是 EdiText 的子类,实际 xml 属性的设置没有太多不同。其他自有的属性有:
属性 | 作用 |
---|
android:completionHint | 下拉列表标题 |
android:completionHintView | 下拉列表项的布局资源 |
android:completionThreshold | 自动补全所需的最小输入字符个数 |
android:dropDownAnchor | 下拉列表的锚点 |
android:dropDownHeight | 下拉列表高度 |
android:dropDownWidth | 下拉列表宽度 |
android:dropDownHorizontalOffset | 下拉列表水平边距 |
android:dropDownSelector | 下拉列表被选中的行的背景色 |
android:dropDownVerticalOffset | 下拉列表竖直边距 |
android:popupBackground | 下拉列表的背景色 |
这其中最常用的是 android:completionThreshold
。
另外,可以看到 xml 中没有设置补全内容的属性,实际上补全内容是通过 Java 代码设置,实现的方式是通过 AutoCompleteTextView 的 setAdapter
方法传入特点 Adapter
实例(一般是 ArrayAdapter
实例)。
官网给出的常见使用代码样例为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class CountriesActivity extends Activity {
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.countries);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_dropdown_item_1line, COUNTRIES);
AutoCompleteTextView textView = (AutoCompleteTextView)
findViewById(R.id.countries_list);
textView.setAdapter(adapter);
}
private static final String[] COUNTRIES = new String[] {"Belgium", "France", "Italy", "Germany", "Spain"};
}
|
这样,就把 AutoCompleteTextView 中最核心的功能自动补全的责任转移到了 Adapter
之上,控件则只负责显示内容即可。如果有自定义的补全逻辑,则可以通过实现新的 Adapter 来实现。
注意:实际使用中,自定义的补全逻辑也不应该通过继承 AutoCompleteTextView 之后修改它的显示逻辑来实现,而只应该通过自定义 Adapter
子类来达成。
一般来说自定义的 Adapter
继承自 ArrayAdpater
就可以,而最核心的更改补全后的结果则是通过 Filter
的子类来实现的。
下面以我刚刚写的简单的 email 地址补全功能为例说明实现方法。
首先,把补全内容写入 xml 格式的字符串数组中(在 res/values/arrays.xml 中)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <string-array name="common_domains">
<!-- Default domains -->
<item>gmail.com</item>
<item>live.com</item>
<item>outlook.com</item>
<item>hotmail.com</item>
<item>yahoo.com</item>
<item>mail.com</item>
<item>email.com</item>
<!-- Domains used in China -->
<item>sina.com</item>
<item>qq.com</item>
<item>163.com</item>
</string-array>
|
然后,自定义 Adapter
:
1
2
3
4
5
6
7
8
9
10
11
12
13
| final class EmailAdapter extends ArrayAdapter<String> {
private Set<String> mDomains;
... // 其他方法
@Override public Filter getFilter() {
return new EmailAutoCompleteFilter();
}
private class EmailAutoCompleteFilter extends Filter {
... // 补全逻辑核心代码处
}
}
|
最重要的是,覆盖 Filter
子类中的 performFiltering
方法用来完成字符串筛选工作,最后覆盖 publishResults
来更改结果,实际代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| private class EmailAutoCompleteFilter extends Filter {
@Override protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
if (constraint == null || constraint.length() == 0) {
setFilterResultsFromSet(results, mDomains);
} else {
final String input = constraint.toString().trim();
// more than one @ in input
if (input.indexOf('@') != input.lastIndexOf('@')) {
setFilterResultsFromSet(results, Collections.<String>emptySet());
} else {
Set<String> resultValues = new TreeSet<>();
String[] strings = input.split("@");
String name = strings[0];
// Note: null is invalid as String.starWith(null) throws NPE
String domainPrefix = strings.length > 1 ? strings[1] : "";
for (String domain : mDomains) {
if (domain.startsWith(domainPrefix)) {
resultValues.add(name + "@" + domain);
}
}
setFilterResultsFromSet(results, resultValues);
}
}
return results;
}
private void setFilterResultsFromSet(FilterResults results,
final Set<String> resultValues) {
results.values = resultValues;
results.count = resultValues.size();
}
@Override protected void publishResults(CharSequence constraint, FilterResults results) {
setNotifyOnChange(false);
clear();
//noinspection unchecked
addAll((TreeSet<String>) results.values);
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
|
代码实现的功能比较简单:
performFiltering
筛选输入文字,分情况转化为补全结果- 如果输入多个 @ 符号,则不给出任何补全结果
- 如果输入内容没有 @ 符号,则将输入拼接上 @ 符与所有域名,给出补全结果
- 如果输入有 1 个 @ 符号,则提取 @ 后面的字符,查找匹配它的域名补全为 @ 后的域名结果
publishResults
将原有内容清除,存入筛选后的内容,然后通知 Adapter
数据已被更改
这里要注意的是:clear
方法会触发 notifyDataSetChanged()
,但是更改结果并不需要两次通知动作,所以先使用 setNotifyOnChange(false);
来关闭 clear
方法的自动通知。
还有,results.values 的类型为 Object
,实际赋值、使用的时候需要注意类型转换。
这样,补全逻辑和显示功能就完全分离,即使日后想要更好补全逻辑也非常方便,而且这样的代码也很方便测试。