在我的项目中在使用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数据源在联网部分随时刷新弹出提示。
代码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后,任然没有实现效果,产生的效果是每次输入的下次才能看到添加的提示内容。
当时我想到的是 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()上。
使用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的深度使用,从源码的角度找到问题,适度自定义,满足开发需求。比较网络看到很多篇博客写得更深入,当有同学遇到和我一样的需求时候,希望能够帮到大家。