AutoCompleteTextView的使用和源码分析以及实现响应式输入提示功能

来源:互联网 发布:淘宝改后台软件安全吗 编辑:程序博客网 时间:2024/05/16 19:25

在我的项目中在使用AutoCompleteTextView用来为用户输入提示。提示内容是来自网络返回,效果如下
效果图
这篇博客记录我的分析和编码过程

1.简单AutoCompleteTextView使用

简单代码示例1:

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"     }; }

这是官网最简答的实例。
说明一下直接运行的话要输入两个字符以上才能弹出提示,这是默认的配置在源码中看到
mThreshold = a.getInt(R.styleable.AutoCompleteTextView_completionThreshold, 2);就说明没有设置默认值是2。一般的使用时需要修改一些默认属性设置
AutoCompleteTextView关键属性说明:

AutoCompleteTextView常用属性 android:completionHint 设置出现在下拉菜单中的提示标题 android:completionThreshold 设置用户至少输入多少个字符才会显示提示 android:dropDownHorizontalOffset 下拉菜单于文本框之间的水平偏移。默认与文本框左对齐 android:dropDownHeight 下拉菜单的高度 android:dropDownWidth 下拉菜单的宽度 android:dropDownVerticalOffset 垂直偏移量 android:popupBackground 设置弹出列表的背景

有了上面的说明一般的使用控件就没有问题,但是我的项目需求是填充内容来自网络随时更新,不能在代码中写死。通过分析上面的实例我当时就想到定义全局的Adapter和mListHttpHint数据源在联网部分随时刷新弹出提示。

2.实现上面的构想

代码1:

 mACTVSearch.addTextChangedListener(new TextWatcher() {            @Override            public void beforeTextChanged(CharSequence s, int start, int count, int after) {                //输入前应该有清空 mListHttpHint的数据操作                mListHttpHint.clear();            }            @Override            public void onTextChanged(CharSequence s, int start, int before, int count) {            }            @Override            public void afterTextChanged(Editable s) {                //用户输入完成后调用 在这里取得输入结果 联网                mListHttpHint.addAll(gethttpHint(s));//将联网返回结果添加进数据集合                mAdapter.notifyDataSetChanged();            }        });

上面代码只说明大概思路,不考虑联网回调同步问题。但是运行上面结果一直得不到想要的效果。联网成功返回但是mAdapter.notifyDataSetChanged()没有效果,没有弹出输入提示,后来改用ArrayAdapter<String>的addAll()方法
源码如下:

public void addAll(T ... items) {        synchronized (mLock) {            if (mOriginalValues != null) {                Collections.addAll(mOriginalValues, items);            } else {                Collections.addAll(mObjects, items);            }        }        if (mNotifyOnChange) notifyDataSetChanged();    }

明显看到在ArrayAdapter<String>中添加数据也会调用通知更新方法。修改代码1后,任然没有实现效果,产生的效果是每次输入的下次才能看到添加的提示内容。

思考解决办法1 从notifyDataSetChanged()切入

当时我想到的是 notifyDataSetChanged()没有用,应该是方法没有成功通知界面刷新,重写过BaseAdapter的同学应该都知道这个方法的使用。BaseAdapter采用观察者模式,当和BaseAdapter绑定的数据集修改之后,直接调用notifyDataSetChanged()方法会产生UI界面重绘效果。
BaseAdapter源码如下:

public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {    private final DataSetObservable mDataSetObservable = new DataSetObservable();//目标对象 作为被观察者    public boolean hasStableIds() {        return false;    }    public void registerDataSetObserver(DataSetObserver observer) {        mDataSetObservable.registerObserver(observer);//注册观察者对象    }    public void unregisterDataSetObserver(DataSetObserver observer) {        mDataSetObservable.unregisterObserver(observer);//反注册对象    }    /**     * Notifies the attached observers that the underlying data has been changed     * and any View reflecting the data set should refresh itself.     */    public void notifyDataSetChanged() {        mDataSetObservable.notifyChanged();//通知注册的观察者对象    }    /**     * Notifies the attached observers that the underlying data is no longer valid     * or available. Once invoked this adapter is no longer valid and should     * not report further data set changes.     */    public void notifyDataSetInvalidated() {        mDataSetObservable.notifyInvalidated();//通知数据无效    }    public boolean areAllItemsEnabled() {        return true;    }    public boolean isEnabled(int position) {        return true;    }    public View getDropDownView(int position, View convertView, ViewGroup parent) {        return getView(position, convertView, parent);    }    public int getItemViewType(int position) {        return 0;//返回item视图的类型 一般多种item视图的adapter会重写该方法,多种视图常见的使用 是聊天界面一样的左右文字显示    }    public int getViewTypeCount() {        return 1;//返回视图类型的数量 重写getItemViewType也要跟着重写    }    public boolean isEmpty() {        return getCount() == 0;    }}

目前使用ArrayAdapter<T> extends BaseAdapter就是子类ArrayAdapter也具有一样的通知更新方法。并且给代码1添加数据后调用mAdapter.getCount()log输出后也看到的adapter中数据集数量发生的变化。说明问题不在notifyDataSetChanged()上。

思考解决办法2 从AutoCompleteTextView的过滤行为切入

使用AutoCompleteTextView时我直接使用了ArrayAdapter,传入系统布局文件和数据源

 mAdapter = new SearHintAdapter(mContext,                android.R.layout.simple_spinner_dropdown_item,mListHttpHint);        mACTVSearch.setAdapter(mAdapter);

class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter从类的继承和实现上看到 ArrayAdapter还实现了Filterable接口
Filterable说明:

Defines a filterable behavior. A filterable class can have its data constrained by a filter. Filterable classes are usually Adapter implementations.

大概意思是:这是个过滤行为,实现的类应该能够过滤数据,adapter类都实现了该接口

在ArrayAdapter源码的470行中也确实重写了getFilter()方法返回一个自定义的过滤器

public Filter getFilter() {        if (mFilter == null) {            mFilter = new ArrayFilter();        }        return mFilter;    }

自定义的ArrayFilter重写了两个方法:
1.过滤方法实现

 @Override        protected FilterResults performFiltering(CharSequence prefix) {        //....省略代码 数据初始化和检查输入代码        //关键代码:根据约束条件调用一个工作线程过滤数据。子类必须实现该方法来执行过滤操作。过滤结果以Filter.FilterResults的形式返回        //参数说明:CharSequence  prefix 输入项也就是用户输入在TextView的内容。ArrayList values 就是Adapter中对原始数据mObjects的复制对象        // 最最关键!valueText.startsWith(prefixString) 匹配输入项的第一个字符 匹配成功的对象放入返回对象中                 final int count = values.size();                final ArrayList<T> newValues = new ArrayList<T>();//新建一个用来存储返回结果ArrayList对象         for (int i = 0; i < count; i++) {                    final T value = values.get(i);                    final String valueText = value.toString().toLowerCase();                    // First match against the whole, non-splitted value                    if (valueText.startsWith(prefixString)) {                        newValues.add(value);                    } else {                        final String[] words = valueText.split(" ");                        final int wordCount = words.length;                        // Start at index 0, in case valueText starts with space(s)                        for (int k = 0; k < wordCount; k++) {                            if (words[k].startsWith(prefixString)) {                                newValues.add(value);                                break;                            }                        }                    }                    //包装返回对象                 results.values = newValues;                results.count = newValues.size();                return results;}

2.发布数据方法

 @Override        protected void publishResults(CharSequence constraint, FilterResults results) {        // results 方法1返回结果对象        //检查返回对象 当大于0个数据时候 调用notifyDataSetChanged()更新UI            //noinspection unchecked            mObjects = (List<T>) results.values;            if (results.count > 0) {                notifyDataSetChanged();            } else {                notifyDataSetInvalidated();            }        }

看到上面的源码终于找到了解决问题的关键。当我们使用简答代码示例1直接使用AutoCompleteTextView的时候,每次输入,系统就会从源数据中找匹配项,然后通知更新UI。这是最简单的使用,系统已经为我们定义好了过滤行为和更新方法。我在代码1中添加了数据项,但是输入没有匹配成功,所以结果没有显示也就没有更新UI。

解决问题

通过上面分析就知道问题出在了过滤行为上,我的需求是每次用户输入根据输入内容从网络调取提示内容,更新到UI。添加到adapter每项数据是最新联网结果,不需要过滤数据。
1,自定义继承ArrayAdapter的SearchHintAdapter,自定义不会过滤掉任何数据的mFilter

import android.content.Context;import android.widget.ArrayAdapter;import android.widget.Filter;import java.util.ArrayList;import java.util.List;/** * Created by LiCola on  2016/03/21  0:41 */public class SearHintAdapter extends ArrayAdapter<String> {    private static final String TAG = "SearHintAdapter";    private Filter mFilter;    private List<String> mObjects;    public SearHintAdapter(Context context, int resource, List<String> objects) {        super(context, resource, objects);        mObjects = objects;    }    @Override    public Filter getFilter() {        if (mFilter == null) {            mFilter = new HintFilter();        }        return mFilter;    }    /**     * <p>An array filter constrains the content of the array adapter with     * a prefix. Each item that does not start with the supplied prefix     * is removed from the list.</p>     * 重写过滤类 自定义一个不会过滤任何数的Filter     */    private class HintFilter extends Filter {        @Override        protected FilterResults performFiltering(CharSequence prefix) {            ArrayList<Object> suggestions = new ArrayList<Object>();            for (String s : mObjects) {                suggestions.add(s);//                Logger.d(s);            }            FilterResults filterResults = new FilterResults();            filterResults.values = suggestions;            filterResults.count = suggestions.size();//            Logger.d("filterResults.count=" + filterResults.count);            return filterResults;        }        @Override        protected void publishResults(CharSequence constraint, FilterResults results) {            //noinspection unchecked            mObjects = (List<String>) results.values;//            Logger.d("results.count=" + results.count);            if (results.count > 0) {                notifyDataSetChanged();            } else {                notifyDataSetInvalidated();            }        }    }}

2.监听AutoCompleteTextView的输入操作,将联网数据添加到mAdapter中

 RxTextView.textChanges(mACTVSearch)//观察mACTVSearch的输入变化                .observeOn(Schedulers.io())                .filter(new Func1<CharSequence, Boolean>() {                    @Override                    public Boolean call(CharSequence charSequence) {                        return charSequence.length()>0;//过滤空输入                    }                })                //debounce 函数 过滤掉由Observable发射的速率过快的数据                .debounce(300, TimeUnit.MILLISECONDS)                //switchMap函数 每当源Observable发射一个新的数据项(Observable)时,                //它将取消订阅并停止监视之前那个数据项产生的Observable,并开始监视当前发射的这一个。                .switchMap(new Func1<CharSequence, Observable<SearchHintBean>>() {                    @Override                    public Observable<SearchHintBean> call(CharSequence charSequence) {                        return getSearHit(charSequence.toString());//Retrofit开始联网 直接返回Observable<SearchHintBean>                    }                })                .map(new Func1<SearchHintBean, List<String>>() {                    @Override                    public List<String> call(SearchHintBean searchHintBean) {                        return searchHintBean.getResult();//转换联网返回结果                    }                })                .filter(new Func1<List<String>, Boolean>() {                    @Override                    public Boolean call(List<String> strings) {                        return strings.size()>0;//过滤联网空返回                    }                })                .observeOn(AndroidSchedulers.mainThread())                .subscribe(new Subscriber<List<String>>() {                    @Override                    public void onCompleted() {                        Logger.d();                    }                    @Override                    public void onError(Throwable e) {                        Logger.d(e.toString());                    }                    @Override                    public void onNext(List<String> strings) {                        Logger.d(strings.size()+"");                        mAdapter.addAll(strings);                    }                });

上面代码用到Retrofit联网框架,RxAndroid和RxBinding响应式编码,很好的优化了代码,不了解的同学可以点击链接学习。
3.附上布局文件

<AutoCompleteTextView                android:id="@+id/actv_search"                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:hint="@string/hint_search"                android:singleLine="true"                android:maxLines="1"                 android:inputType="textAutoComplete"                android:completionThreshold="1"//中文输入的话最好修改为1                android:popupBackground="@color/white"                />

总结

这是对AutoCompleteTextView的深度使用,从源码的角度找到问题,适度自定义,满足开发需求。比较网络看到很多篇博客写得更深入,当有同学遇到和我一样的需求时候,希望能够帮到大家。

1 0
原创粉丝点击