AutoCompleteTextView的使用和源碼分析以及實現響應式輸入提示功能

我的項目中在使用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聯網框架,RxAndroidRxBinding響應式編碼,很好的優化了代碼,不瞭解的同學可以點擊鏈接學習。
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的深度使用,從源碼的角度找到問題,適度自定義,滿足開發需求。比較網絡看到很多篇博客寫得更深入,當有同學遇到和我一樣的需求時候,希望能夠幫到大家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章