在我的項目中在使用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的深度使用,從源碼的角度找到問題,適度自定義,滿足開發需求。比較網絡看到很多篇博客寫得更深入,當有同學遇到和我一樣的需求時候,希望能夠幫到大家。