1. AutoCompleteTextView 到底解决了我什么实际痛点1.1 没有自动完成的年代输入框让我有多头疼早些年做工具类应用时我遇到过一个特别现实的场景给用户做城市选择。数据量大概三百多个城市如果直接放一个 Spinner 下拉列表用户从里面翻找要滚动很久如果放一个 EditText 让用户自己敲经常有人把乌鲁木齐打成乌鲁木其匹配不上就白费功夫。就在这种进退尴尬的处境里AutoCompleteTextView 这种边输入边提示的自动完成文本框成了最合适的选择。它本质上不是一个全新的控件而是继承自 EditText在普通输入框之上叠加了一层输入联想逻辑。用户敲入几个字符后控件会拿当前已输入的文本去过滤一个预设的数据集合然后在下拉面板中展示命中的候选条目点击任意一条就能把完整内容回填进输入框。这个交互模式在日常应用里太常见了——搜索框、邮箱输入、标签填写、城市选择、商品搜索记录几乎都是这套逻辑。所以开篇先明确适用范围AutoCompleteTextView 适合数据源已知、枚举值有限用户希望少敲键盘的场景。它的核心价值不是帮你做算法推荐引擎而是把从输入到命中之间的体验鸿沟填平让用户少打几个字、少犯几次拼写错误。这篇文章我会从零开始讲清楚布局怎么写、适配器怎么接、数据怎么过滤、下拉样式怎么定制以及实际项目中容易被忽略的配置细节和坑点。1.2 输入框、下拉弹层、过滤触发它是怎么配合工作的我一直觉得理解一个新控件最快的办法是把它拆成三个部件来看。AutoCompleteTextView 可以简单地分三层认知最表层的输入框就是大家熟悉的 EditText 行为中层是它内置的下拉弹层由 PopupWindow 的思想演化而来底层是一套数据源 过滤器 回调接口的组合逻辑。在 Android 的源码结构里AutoCompleteTextView 的核心成员很精简一个数据集适配器 ListAdapter一个数据过滤器 Filter一个描述当前输入与候选集之间匹配规则的内部逻辑以及一个下拉列表的展示容器。用户每敲一个字符输入框文本发生变化就会触发一次数据刷新请求。这个请求交给适配器持有的 Filter 对象执行过滤再把过滤后的结果塞回下拉列表里重新渲染。这套分工机制让它和普通 EditText 有一个关键区别EditText 只关心用户输入了什么AutoCompleteTextView 额外关心当前输入能关联到哪些候选。所以它的数据层天然围绕 Adapter 转布局里常见的 android:completionThreshold 属性决定了用户至少要输入几个字符下拉列表才肯露面。这个门槛设得太低数据稍微多一点的场景人还没打完一个字匹配列表就先糊了一屏幕设得太高联想功能就形同虚设。某种程度上AutoCompleteTextView 的调优过程就是在这两个极端之间找平衡。1.3 哪些场景它最顺手哪些场景建议你换人在实际开发里我总结过几类适合它的典型用法。第一类是枚举型数据输入比如性别、部门、车型、城市名称数据量几百条以内用 AutoCompleteTextView 能让输入速度明显提升。第二类是带历史记录的输入比如搜索框联想用户之前搜过的关键词数据量即便到几千条只要做了本地过滤和索引跑起来依然流畅。第三类是邮箱、域名、标签这类强规律文本配合 MultiAutoCompleteTextView 多段输入可以做到分段联想。反过来说如果数据源是几万条实时从服务器拉回来的商品列表或者需要分词、拼音缩写、模糊音匹配这类复杂检索逻辑AutoCompleteTextView 自带的 Filter 机制就不太够用了。它不是搜索引擎它的过滤本质上是给定一个字符串在候选集里找出包含它的条目。真要处理大规模模糊搜索不如把数据和匹配逻辑拿到后端去做再通过接口回传候选集用自定义的 BaseAdapter 喂给它展示。选型想清楚了后面写的每一行代码才不容易白费。2. 搭一个能跑起来的自动补全总共分几步2.1 布局文件里的核心配置一个都别漏先从最直观的布局部分开始。AutoCompleteTextView 在 XML 里的声明方式和 EditText 几乎一致因为它就是一个带增强功能的输入框。最基础的写法如下AutoCompleteTextView android:idid/actv_city android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入城市名称 android:completionThreshold1 android:dropDownWidthmatch_parent android:dropDownHeightwrap_content android:backgrounddrawable/bg_input_normal /这里有几个属性值得多说两句。completionThreshold 是指输入多少个字符后开始弹出联想我建议至少设为 1也就是说敲一个字就出现候选。这个值取决于你的数据源规模和控件的使用频率搜索类场景可以设为 1表单类场景我喜欢设成 2因为像广东和广西这种一两个字就能命中的场景候选列表太灵敏反而干扰用户输入。dropDownWidth 和 dropDownHeight 是很多人忽略的点。默认情况下下拉列表的宽度和输入框一致但有些主题下会出现宽度超出屏幕的奇怪视觉问题这时候给它显式设置一个 match_parent 或者制定一个具体的数值通常会更快归一。dropDownVerticalOffset 可以调整下拉面板和输入框之间的空隙某些界面上两个元素贴得太近用这个属性加一点垂直间距之后视觉上会舒服很多。2.2 接入数据为什么我首推 ArrayAdapter对于基础入门场景数据接入最简单的方式就是用 ArrayAdapter。这一步理解了后面所有玩法都是在这条主线上做扩展。假设我要做一个城市联想代码可以这样写AutoCompleteTextView actvCity findViewById(R.id.actv_city); String[] cities {北京, 上海, 广州, 深圳, 成都, 杭州, 武汉}; ArrayAdapterString adapter new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, cities); actvCity.setAdapter(adapter);在两三行代码里隐藏着几个容易被忽略的细节。构造方法里的那个布局资源 android.R.layout.simple_dropdown_item_1line指定的是候选列表中每一行条目长什么样默认就是一个黑底或白底的 TextView。如果你想要每一条候选都带个图标或者左侧显示一个类型标签那就要自己写布局文件然后用 ArrayAdapter 的另一个构造方法或者干脆自定义 Adapter。Kotlin 版的写法同样简洁val actvCity findViewByIdAutoCompleteTextView(R.id.actv_city) val cities arrayOf(北京, 上海, 广州, 深圳, 成都, 杭州, 武汉) val adapter ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, cities) actvCity.setAdapter(adapter)这里要提一个很实际的体验问题ArrayAdapter 调用 toString() 来显示条目如果是普通 String 列表自然没问题可如果是自定义对象比如一个 City 类包含中文名、拼音、代码多种字段那么一定要正确覆写 toString()否则下拉列表里会出现一串类似com.example.model.City1a2b3c的乱码。很多人第一次用自定义对象做数据源时都会在这里卡住。2.3 文本变化和候选命中监听器应该怎么挂自动补全的下拉展示是系统帮你搞定的但交互的收尾动作经常需要开发人员介入。最常见的一个需求是用户在下拉列表里选中了某个候选条目接下来要立刻触发一次搜索或者跳转。这就需要挂监听。actvCity.setOnItemClickListener((parent, view, position, id) - { String selected parent.getItemAtPosition(position).toString(); // 此时输入框里的文本已经被替换为 selected Toast.makeText(this, 选中了 selected, Toast.LENGTH_SHORT).show(); });值得留意的是OnItemClickListener 回调的时机是在文本已经回填到输入框之后。所以在这个回调里取输入框文本拿到的就是当前条目的内容不需要再去 read 一遍 EditText 再赋值一次。如果你想在用户手动输入过程中做实时搜索结果的刷新那就得用 TextWatcher 监听输入框的文本变化actvCity.addTextChangedListener(new TextWatcher() { Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} Override public void onTextChanged(CharSequence s, int start, int before, int count) { // 这里可以拿到实时输入内容做其他业务处理 } Override public void afterTextChanged(Editable s) {} });不过要提醒一句TextWatcher 和 AutoCompleteTextView 自带的下拉过滤触发是并行的不是替代关系。系统内置的过滤逻辑依旧会根据输入文本更新候选列表。如果你自己也发网络请求做联想那就要把两种路径都想清楚避免产生重复请求或者新旧数据交替导致的闪烁。2.4 最容易被忽略的 Empty 状态处理我遇到过不少同学做完自动补全后私底下会吐槽有时候没输入任何内容时下拉列表也会弹出来。这个现象其实还有一个常见的原因没有显式控制下拉列表的显示条件。AutoCompleteTextView 的下拉列表弹出条件结合了输入框是否获得焦点、输入文本长度是否达到 threshold以及候选集是否为空。但在某些情况下比如你设置了数据源用户点击输入框后没输入任何字符却发现下拉列表显示出来一整个全集这通常是因为 threshold 被设置成了 0。completionThreshold 设为 0 是合法的但它的含义比较特殊输入框一获得焦点就会展示所有候选集。这个行为不一定是最佳产品设计。如果你希望用户输入才联想就得把 threshold 设置成大于等于 1。真要让下拉列表关掉也没必要去改适配器数据直接调用 dismissDropDown() 即可代码干净简单。3. 数据过滤机制自动补全背后的工作原理与定制空间3.1 从字符变化到列表刷新中间到底发生了什么很多做客户端开发的朋友对 AutoCompleteTextView 的使用停留在setAdapter 就能跑的阶段并不知道它内部做了哪些事。当我第一次去翻源码的时候我才意识到这套联想的链路值得理清。用户敲入一个字符InputConnection 把文本变更事件传递到视图层AutoCompleteTextView 内部实现了一个 TextWatcher监听 afterTextChanged紧接着它会检查当前文本的长度与 completionThreshold 的配置。如果长度不够直接取消下拉显示如果长度达标就从当前适配器里取数据过滤器 Filter发起一次异步过滤。Filter 的过滤动作有个引用计数和同步锁的机制。简单说如果用户输入速度很快前一秒发出的过滤请求还没返回新的请求又产生了系统会通过一个计数器丢弃过期的结果只接受最新一次过滤请求的回调。UI 线程因此不会被大量无效的 data set 变化阻塞。过滤完成后结果通过 publishResults 回到 UI 线程适配器重新绑定数据然后自动弹出下拉面板。整个过程主线程不参与复杂的字符串匹配计算所以即使候选集有上千条数据输入响应也不会卡顿。理解这条链路的意义在于当你在排查为什么我的输入卡了一下、为什么过期的候选数据会闪一下这类问题时会自然而然地往 Filter 的异步机制上想而不是先去怀疑布局渲染。而且你也知道如果数据源非常大真正的瓶颈可能不在过滤本身而是每次过滤都要对整个 List 做一次线性遍历。3.2 默认过滤规则contains、忽略大小写、还是前缀匹配ArrayAdapter 内置的过滤逻辑是通过一个 ArrayFilter 内部类实现的。这个过滤器从候选集合中取词然后逐个检查只要候选字符串包含当前输入的文本就会被保留。这个包含是忽略大小写的也就是说输入 an 可以匹配到 Android、Hangzhou、Shanghai 里的 an。如果你期望的是一种必须从头开始匹配的搜索那这个默认逻辑就不满足需求了。ArrayFilter 的匹配规则在不同 Android 版本上有过细微调整但大方向一致把输入文本转成小写把候选文本转成小写然后用 indexOf 判断是否包含。这个过程对中英文同样有效对拼音则无能为力。你想让用户输入全拼或首字母时也能联想出中文候选那就不能指望默认过滤需要自定义 Filter 重写过滤规则或者把拼音字段也写在候选对象的 toString 里让系统去做包含匹配。我曾经在一个联系人选择页面做过一个比较简单的优化把每个联系人对象 toString() 返回成“姓名 拼音首字母 手机号”的拼接串。这样用户在输入名字的拼音首字母时由于包含了这些字符也能在默认过滤器中得到命中的结果。这种方案的优点是零成本缺点是不够精准输入 zh 会把很多既含 zh 也含 sh 的对象都捞出来。精准一点的做法还是得自己写 Filter。3.3 自定义 Filter打破默认规则的唯一入口如果你决定自己控制过滤规则思路就清晰了。ArrayAdapter 内部通过 getFilter() 返回一个 Filter你只需要覆写 performFiltering 和 publishResults 两个方法。下面是一个简单的自定义 Filter 示例匹配规则从包含改成前缀匹配public class PrefixFilter extends Filter { private ListString sourceData; private ListString filteredData; public PrefixFilter(ListString sourceData) { this.sourceData sourceData; } Override protected FilterResults performFiltering(CharSequence constraint) { FilterResults results new FilterResults(); ListString match new ArrayList(); if (constraint null || constraint.length() 0) { match.addAll(sourceData); } else { String keyword constraint.toString().toLowerCase(); for (String item : sourceData) { if (item.toLowerCase().startsWith(keyword)) { match.add(item); } } } results.values match; results.count match.size(); return results; } Override protected void publishResults(CharSequence constraint, FilterResults results) { filteredData (ListString) results.values; notifyDataSetChanged(); // 注意这里需要重新给 adapter 的内部数据源赋值 } }这里的关键点是 publishResults 之后必须调用 notifyDataSetChanged 才能让列表刷新。很多新手写自定义 Filter 时忘了这一行导致过滤结果永远不显示。而 arraylist 的过滤还有一些边界情况如果 constraint 为空字符串应当如何处理是展示全部还是展示空列表这个完全取决于你自己的业务判断没有标准答案。在我的项目里我倾向于 constraint 为空时也展示全部作用是用户一点击输入框就可以看到完整的可选项形成一种快捷选择面板的体验。自定义 Filter 的另一个常见用途是去服务端查询。你只需要在 performFiltering 里把 constraint 当参数请求后端接口同步等待结果返回然后封装成 FilterResults。但注意performFiltering 是执行在后台线程的你不能在这里直接更新 UI只能在 publishResults 中回到主线程刷新适配器。如果你用 Retrofit 的异步回调来请求那就不能在 performFiltering 里直接 publish要绕一层处理。这种场景我建议直接放弃系统自带的 Filter用一个普通 EditText 配合 RecyclerView 做手动实现或者对 AutoCompleteTextView 做一层封装反而更自由。4. 定制下拉列表的显示让自动补全更贴近业务审美4.1 自定义条目布局从简单一行文本到图文混排用默认的 simple_dropdown_item_1line 布局时下拉面板里只有一行文字看起来单调跟应用的整体风格经常不搭。真正业务里我更常用的是自定义列表项布局。比如做一个搜索历史联想我希望每条候选左侧是时钟图标右侧是搜索关键词再往右一个删除图标。这种需求下自定义 Adapter 是躲不开的。写自定义 Adapter 的原则是继承 BaseAdapter 或 RecyclerView.Adapter 都行但如果你只是想微调布局样式更简单的做法是直接继承 ArrayAdapter 并覆写 getView。下面这个例子能说明最基本的思路public class CityAdapter extends ArrayAdapterCity { public CityAdapter(Context context, ListCity cities) { super(context, 0, cities); } Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView null) { convertView LayoutInflater.from(getContext()) .inflate(R.layout.item_city, parent, false); } TextView nameView convertView.findViewById(R.id.tv_city_name); TextView codeView convertView.findViewById(R.id.tv_city_code); City city getItem(position); nameView.setText(city.getName()); codeView.setText(city.getCode()); return convertView; } }这个 getView 里最需要注意的是 convertView 的复用逻辑。系统通过复用旧视图大幅度减少 inflate 开销如果你每次都在 getView 里重新创建视图列表滑动一快帧率一定往下掉。一个小习惯把能用 final 声明的子视图都在首次创建时缓存下来或者使用 ViewHolder 模式。4.2 给命中文本加高亮色用 SpannableString 提升体验自动补全的下拉列表里用户最关心的是我打的字有没有命中间谍。很多搜索类应用会把命中的关键字在候选条目里标成高亮色这样做有一个好处用户能直观地确认这条联想真的跟自己输入的文字有关。实现思路也有多种。最简单的是在自定义 Adapter 的 getView 里用 SpannableString 给关键字上色String displayText city.getName(); SpannableString spannable new SpannableString(displayText); String keyword getFilter().convertResultToString(getItem(position)); // 这里取的是当前输入关键字 int index displayText.toLowerCase().indexOf(keyword.toLowerCase()); if (index 0) { spannable.setSpan(new ForegroundColorSpan(0xFF2196F3), index, index keyword.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } nameView.setText(spannable);这段代码每次 getView 时执行一次文本拼接。注意一个坑我们在这里取当前输入关键字时不能直接读 getText() 拿输入框的文本因为某些情况下点击候选项后输入框文本会被替换成候选全文导致高亮位置计算错误。更好的做法是给自定义 Adapter 单独存一个字段在 setFilterText 或者数据更新时记录当前过滤关键字这样高亮始终跟随用户真实输入。4.3 控制下拉面板宽高、分隔线、最大高度和动画基础布局之外AutoCompleteTextView 暴露了一些可直接调用的方法帮你微调下拉展示行为。setDropDownWidth() 设置下拉面板宽度setDropDownHeight() 设置高度setDropDownVerticalOffset() 设置垂直偏移量。如果你希望下拉面板有分隔线或者淡入动画就得动它的底层属性。下拉面板的淡入淡出可以通过设置 android:dropDownAnimationStyle 来实现比如我经常写style nameDropdownAnimation item nameandroid:windowEnterAnimationanim/dropdown_enter/item item nameandroid:windowExitAnimationanim/dropdown_exit/item /style AutoCompleteTextView android:dropDownAnimationStylestyle/DropdownAnimation /分隔线通常通过 android:dropDownSelector 设置也可以把下拉面板当作一个 ListView 来理解。其实它内部确实持有一个 ListView 对象通过 setDropDownBackgroundResource() 就能设置背景和分隔线资源。当列表项数量很多时为了防止下拉面板超出屏幕可以用 setDropDownHeight(dp2Px(300)) 限制一个固定的最大高度设置成 wrap_content 时系统会自己根据条目数量调整高度但偶尔也会因为键盘弹出导致遮挡。高度计算这里我建议在布局文件里直接写一个最小值和最大值至少保证屏幕上能看到 4 到 5 个候选条目。5. MultiAutoCompleteTextView同门师兄多段输入的补全5.1 邮箱输入和标签填写里它的价值比 AutoCompleteTextView 更高AutoCompleteTextView 是整段文本做联想用户在任意位置输入时拿整段输入去过滤候选。但遇到多个邮箱一起写或者多个标签一起输入这种场景它就不够用了。比如让用户填写收件人邮箱格式可能是 alice163.com, bobgmail.com用户希望逗号之后换一个新的片段仍然能对每个片段分别联想。这种场景就轮到 MultiAutoCompleteTextView 上场。MultiAutoCompleteTextView 继承自 AutoCompleteTextView核心差异在于它多了一个 Tokenizer 的概念。Tokenizer 的作用是把当前输入框的文本拆分成多个片段并且告诉控件当前光标所在的片段是哪一个。内置实现有 MultiAutoCompleteTextView.CommaTokenizer以逗号作为分隔符。有了它每次输入联想时只针对逗号后面的那一段英文地址做匹配而不是把整串都拿来匹配联想结果的准确度一下子就上来了。假设我的邮箱收集场景候选邮箱列表是有线的常用域名列表比如 163.com、gmail.com、qq.com那么布局和代码可以这样写MultiAutoCompleteTextView android:idid/actv_emails android:layout_widthmatch_parent android:layout_heightwrap_content android:hint请输入收件人邮箱多个邮箱用逗号分隔 android:completionThreshold1 /MultiAutoCompleteTextView actvEmails findViewById(R.id.actv_emails); String[] emails {163.com, gmail.com, qq.com, outlook.com, icloud.com}; ArrayAdapterString emailAdapter new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, emails); actvEmails.setAdapter(emailAdapter); actvEmails.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());这里有一个小坑CommaTokenizer 内部判断分隔符时用的是英文逗号如果你在中文输入法下习惯打全角逗号“”作为分隔CommaTokenizer 不会把它当作分隔于是整个 alice163.comgmail.com 都会被当作一个输入片段导致联想失效。所以如果业务支持中文逗号最好自定义一个 Tokenizer。5.2 写一个支持中文逗号的自定义 Tokenizer自定义 Tokenizer 并不复杂Tokenizer 接口一共三个方法findTokenStart、findTokenEnd、terminateToken。理解这三个方法的意思就能完全控制分段逻辑。findTokenStart 从光标位置往前找到上一个分隔符返回当前片段的起始位置findTokenEnd 从某个位置向后找这段文本的结束位置terminateToken 则用于描述一个片段结束后应该在文本尾部拼接什么分隔符。下面是一个支持中英文逗号的简单实现public class ChineseCommaTokenizer implements MultiAutoCompleteTextView.Tokenizer { Override public int findTokenStart(CharSequence text, int cursor) { int i cursor; while (i 0 text.charAt(i - 1) ! , text.charAt(i - 1) ! ) { i--; } return i; } Override public int findTokenEnd(CharSequence text, int cursor) { int i cursor; while (i text.length() text.charAt(i) ! , text.charAt(i) ! ) { i; } return i; } Override public CharSequence terminateToken(CharSequence text) { int i text.length(); while (i 0 (text.charAt(i - 1) , || text.charAt(i - 1) )) { i--; } if (text instanceof Spanned) { Spannable sp new SpannableString(text.subSequence(0, i) ); return sp; } return text.subSequence(0, i) ; } }这个实现虽然短但已经把 Tokenizer 的交互细节吃透了。注意 terminateToken 里面必须处理 Spannable 的情况否则选中候选条目后文本样式可能丢失。很多从 AutoCompleteTextView 刚转过来的人都会漏掉这个判断导致输入提示颜色花掉。5.3 多段输入的筛选逻辑只看当前段不碰前几段MultiAutoCompleteTextView 的过滤逻辑建立在 Tokenizer 之上。每次输入文本变化它会先拿到光标所在的 token 范围然后用这个范围内的文字去过滤候选集。这跟 AutoCompleteTextView 的全局过滤完全不一样所以如果你使用很长的整段文本千万不要再往里塞关键字数据否则前几个片段的结尾很容易被包含进当前 token过滤结果错乱。实际测试中我给这个邮箱输入框接入了上千个域名候选配合自定义 Tokenizer用户的体验和原生 Gmail 的收件人输入几乎一致。另外一个注意点是多段输入场景里如果用户在片段中间插入文本或者删除某个片段Tokenizer 的切换也要跟上光标位置否则输入不流畅。这类问题本质上都是 Tokenizer 返回值不够严谨导致的排错时先打日志看 findTokenStart 返回的位置是否符合预期。6. 实测中绕不开的三类问题键盘遮挡、性能卡顿、误触选择6.1 输入法弹窗和下拉面板打架怎么协调才不跳屏AutoCompleteTextView 的下拉面板本质是悬浮在窗口上的所以当输入法弹出时面板必然需要根据屏幕底部的可用空间重新计算位置。我这里遇到过最典型的问题是键盘弹出后下拉面板有一部分被键盘盖住用户看不到完整的候选列表。解决思路有几条。最常用的是在 AndroidManifest.xml 中给 Activity 设置 windowSoftInputMode 为 adjustResize让整个布局在键盘弹出时重新分配空间下拉面板随之被顶上去。不过有时 adjustResize 在横屏或全屏模式下不一定生效你需要检查状态栏和导航栏的配置。另一个思路是动态计算输入框在屏幕中的位置然后给下拉面板设置一个合适的高度。粗笨但可靠的办法是做成输入时全屏弹层把完整候选列表放到一个 Dialog 或 Fragment 中展示这样天然避免键盘遮挡问题。这些方案选型的根据很简单表单页面条目少优先用 adjustResize搜索页面要显示大量联想结果优先用独立弹层横屏适配紧张的项目干脆给下拉面板一个固定最大高度比如键盘上方的 60% 空间。没有万能解只有最匹配场景的手感。6.2 候选集很大时内存和卡顿怎么平衡AutoCompleteTextView 内置的 ArrayAdapter 在数据量很大时有一个内存隐患每次过滤都会复制一份 List 对象如果用户连续删除再输入频繁创建中间 List 会造成一定的 GC 压力。我在一个城市选择页里塞了 3000 多个乡镇名称用默认 ArrayAdapter 跑明显能感觉到输入时偶发卡顿。性能优化的第一个方向是限制候选集规模。用户输入的过滤结果最多展示前 20 条或者前 50 条超出部分直接截断。这既保证了 UI 渲染流畅也更符合用户预期——没人会把几百条候选翻完尤其在下拉列表这种一屏只能放七八条的空间里。第二个方向是复用自定义数据源和 Filter避免每次过滤都新建 Filter也不要在 performFiltering 里做重量级对象创建。第三如果数据源本身超过一定规模比如上万条我建议把数据预加载到一个 HashMap 索引中过滤时用二分查找或者直接遍历索引而不是每次都拿 List 遍历匹配。另外还有一个关于渲染的细节自定义 getView 时尽量避免在条目布局里做过多嵌套层级。单个条目用一个根布局TextView 和 ImageView 直接平铺不要把条目设计成三层嵌套的复杂卡片。下拉列表的滚动性能会因此受益明显。6.3 OnItemClick 的误触与数据回填时机别在这里踩坑AutoCompleteTextView 的 OnItemClick 回调里系统已经把选中值回填到输入框了。这里有一个隐蔽的问题如果用户在下拉面板上用方向键或触控笔移动光标按 Enter 可能会触发默认的点击行为导致选中的值和回填文本不一致。这个问题在模拟器上复现过几次最后是打开 onKeyDown 处理主动拦截 ENTER 键事件才稳住。另外有些业务会同时监听 TextWatcher 和 OnItemClick。请留意 TextWatcher 在 OnItemClick 之后也会被再次触发因为回填文本是一次文本变更。如果你在 TextWatcher 里发起了网络请求那么用户点击候选条目时就可能在 setText 的过程中又触发一个额外请求。这种双触发在列表类页面上简直防不胜防。我的经验是在 onItemClick 里设置一个标志位告诉 TextWatcher 本次文本变化是程序引起的忽略掉这次回调或者干脆把所有业务逻辑收敛到 onItemClick 里不再监听 TextWatcher。这类交互细节没有标准答案核心是理解事件触发顺序用户点击条目先更新输入框文本再触发 TextWatcher最后回调 onItemClick。你设计逻辑时要清楚当前处于哪个时机才能避免数据源来回覆盖的困惑。7. 最后再分享一个实用思路用动态数据源把它做成真联想的入口我把 AutoCompleteTextView 用在搜索记录联想中的一个做法是一个很好的收尾。搜索历史通常保存在本地数据库里数据量不大但更新频繁。我的项目里先用 Room 查最近 20 条历史存入 ArrayList再创建一个 ArrayAdapter 放进 AutoCompleteTextView。用户每搜索一次就会把新词插到列表头部并更新数据库下一次进入页面时输入框获得焦点不输入字符历史记录就能以候选列表的形式自动弹出来。这个交互对整个产品来说几乎完全不增加门槛但用户每天的搜索成本直线下降。如果新词插入位置在头部记得在更改列表后立即调用 adapter.notifyDataSetChanged()。此时过滤并没发生系统不会自动刷新下拉列表不调用的话用户看不到这次更新。还有一点动态数据源状态下用户的输入可能命中零候选那时下拉列表会自动关闭不用额外处理。但如果你希望在这种状态给出一个提示文案比如无匹配记录就需要在自定义 Adapter 里返回一个空文本占位条目再结合 setOnClickListener 做提示这套扩展玩法彻底跑通了数据的流转闭环也就完整了。每次我在新项目里再接触到输入联想需求时都会优先想起 AutoCompleteTextView 这套机制。多数场景下它已经足够可靠剩下的功夫都在于你对布局细节、适配器选型和事件时序的把控。按上面的顺序一步步搭我相信你也能很快把它跑顺并找到自己业务里最顺手的那套用法。
阅读完成 · 觉得有帮助?