目录

Android 控件学习之 AutoCompleteTextView

目录

在做登录模块 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,实际赋值、使用的时候需要注意类型转换。

这样,补全逻辑和显示功能就完全分离,即使日后想要更好补全逻辑也非常方便,而且这样的代码也很方便测试。