概述
在Android開發中,當系統數據項比較多時,常常會在app添加搜索功能,方便用戶能快速獲得需要的數據。搜索欄對于我們并不陌生,在許多app都能見到它,比如豌豆莢

在某些情況下,我們希望我們的自動補全信息可以不只是純文本,還可以像豌豆莢這樣,能顯示相應的圖片和其他數據信息,因此Android給我們提供的AutoCompleteTextView往往就不夠用,在大多情況下我們都需要自己去實現搜索框。
分析
根據上面這張圖,簡單分析一下自定義搜索框的結構與功能,有
1. 搜索界面大致由三部門組成,如圖:輸入框+(自動補全)提示框+結果列表。
2. 提示框的數據與輸入框輸入的文本是實時聯動的,而結果列表只有在每次進行搜索操作時才會更新數據
3. 輸入框的UI應是動態的,即UI隨著輸入的文本的改變而改變,如:在未輸入文本時,清除按鈕
應該是隱藏的;只有當框中有文本時才會顯示。
4. 軟鍵盤也應該是動態的,如完成搜索時應自動隱藏。
5. 選擇提示框的選項會自動補全輸入框,且自動進行搜索
6. (external)有熱門搜索推薦/記錄搜索記錄的功能——熱門搜索推薦列表只在剛要進行搜索的時候彈出,即未輸入文本時,可供用戶選擇。
根據上面的分析,我們認為一個搜索框應該包含輸入框和提示框兩個部分。搜索框可以設置一個回調監聽接口,當需要進行搜索操作時,調用監聽者的search()方法,從而實現具體的搜索操作以及結果列表的數據聯動。
演示Demo

注意:
1. 這里,博主圖方便沒有模擬太多數據,而且提示框和熱搜列表也都只是使用String類型的數據,各位看官們可以根據自身需要去設置item_layout和相應的adapter。
2. 由于個人習慣,博主在這個demo中使用了通用適配器,所以生成和設置adapter的代碼比較簡略,看官們可以根據傳統的ViewHolder模式打造自己的adapter。或者學習一下通用適配器的打造。可以參考這里(鴻神博客Again)學習一下通用適配器的打造,在我的源碼里面也有對應的源碼。
實現
好了,說了那么多,開始來看代碼吧
先看SearchView的布局文件 search_layout.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:background="#eee" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:background="#eb4f38" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <FrameLayout android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content"> <EditText android:id="@+id/search_et_input" android:layout_gravity="center_vertical" android:layout_margin="10dp" android:drawableLeft="@drawable/search_icon" android:drawablePadding="5dp" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/search_edittext_shape" android:textSize="16sp" android:imeOptions="actionSearch" android:inputType="text" android:hint="請輸入關鍵字"/> <ImageView android:visibility="gone" android:layout_marginRight="20dp" android:src="@drawable/iv_delete_bg" android:id="@+id/search_iv_delete" android:layout_gravity="right|center_vertical" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </FrameLayout> <Button android:id="@+id/search_btn_back" android:layout_marginRight="10dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:layout_gravity="center_vertical" android:background="@drawable/btn_search_bg" android:layout_width="@dimen/btn_width" android:layout_height="@dimen/btn_height" android:text="返回" android:textColor="@color/color_white"/> </LinearLayout> <ListView android:visibility="gone" android:id="@+id/search_lv_tips" android:background="@drawable/lv_search_tips_bg" android:layout_marginLeft="20dp" android:layout_marginRight="20dp" android:layout_marginBottom="10dp" android:layout_width="match_parent" android:layout_height="200dp"> </ListView> </LinearLayout>
注意:demo中顏色什么的都直接用的rgb 值去設置,在實際開發時,需要把它們都統一管理到values目錄下 。
比較簡單,需要注意的是EditText的這個屬性
android:imeOptions="actionSearch"
就是把Enter鍵設置為Search鍵,并把點擊Enter鍵的動作設為actionSearch,這樣既可在代碼中監聽何時按下search鍵
沒什么說的,bg屬性可以直接看看源碼。接下來看模擬的bean類,這里直接就叫Bean.java
public class Bean { private int iconId; private String title; private String content; private String comments; public Bean(int iconId, String title, String content, String comments) { this.iconId = iconId; this.title = title; this.content = content; this.comments = comments; } public int getIconId() { return iconId; } public void setIconId(int iconId) { this.iconId = iconId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getComments() { return comments; } public void setComments(String comments) { this.comments = comments; } } 接著看主角SearchView.java
public class SearchView extends LinearLayout implements View.OnClickListener { /** * 輸入框 */ private EditText etInput; /** * 刪除鍵 */ private ImageView ivDelete; /** * 返回按鈕 */ private Button btnBack; /** * 上下文對象 */ private Context mContext; /** * 彈出列表 */ private ListView lvTips; /** * 提示adapter (推薦adapter) */ private ArrayAdapter<String> mHintAdapter; /** * 自動補全adapter 只顯示名字 */ private ArrayAdapter<String> mAutoCompleteAdapter; /** * 搜索回調接口 */ private SearchViewListener mListener; /** * 設置搜索回調接口 * * @param listener 監聽者 */ public void setSearchViewListener(SearchViewListener listener) { mListener = listener; } public SearchView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; LayoutInflater.from(context).inflate(R.layout.search_layout, this); initViews(); } private void initViews() { etInput = (EditText) findViewById(R.id.search_et_input); ivDelete = (ImageView) findViewById(R.id.search_iv_delete); btnBack = (Button) findViewById(R.id.search_btn_back); lvTips = (ListView) findViewById(R.id.search_lv_tips); lvTips.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) { //set edit text String text = lvTips.getAdapter().getItem(i).toString(); etInput.setText(text); etInput.setSelection(text.length()); //hint list view gone and result list view show lvTips.setVisibility(View.GONE); notifyStartSearching(text); } }); ivDelete.setOnClickListener(this); btnBack.setOnClickListener(this); etInput.addTextChangedListener(new EditChangedListener()); etInput.setOnClickListener(this); etInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { if (actionId == EditorInfo.IME_ACTION_SEARCH) { lvTips.setVisibility(GONE); notifyStartSearching(etInput.getText().toString()); } return true; } }); } /** * 通知監聽者 進行搜索操作 * @param text */ private void notifyStartSearching(String text){ if (mListener != null) { mListener.onSearch(etInput.getText().toString()); } //隱藏軟鍵盤 InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); } /** * 設置熱搜版提示 adapter */ public void setTipsHintAdapter(ArrayAdapter<String> adapter) { this.mHintAdapter = adapter; if (lvTips.getAdapter() == null) { lvTips.setAdapter(mHintAdapter); } } /** * 設置自動補全adapter */ public void setAutoCompleteAdapter(ArrayAdapter<String> adapter) { this.mAutoCompleteAdapter = adapter; } private class EditChangedListener implements TextWatcher { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { } @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { if (!"".equals(charSequence.toString())) { ivDelete.setVisibility(VISIBLE); lvTips.setVisibility(VISIBLE); if (mAutoCompleteAdapter != null && lvTips.getAdapter() != mAutoCompleteAdapter) { lvTips.setAdapter(mAutoCompleteAdapter); } //更新autoComplete數據 if (mListener != null) { mListener.onRefreshAutoComplete(charSequence + ""); } } else { ivDelete.setVisibility(GONE); if (mHintAdapter != null) { lvTips.setAdapter(mHintAdapter); } lvTips.setVisibility(GONE); } } @Override public void afterTextChanged(Editable editable) { } } @Override public void onClick(View view) { switch (view.getId()) { case R.id.search_et_input: lvTips.setVisibility(VISIBLE); break; case R.id.search_iv_delete: etInput.setText(""); ivDelete.setVisibility(GONE); break; case R.id.search_btn_back: ((Activity) mContext).finish(); break; } } /** * search view回調方法 */ public interface SearchViewListener { /** * 更新自動補全內容 * * @param text 傳入補全后的文本 */ void onRefreshAutoComplete(String text); /** * 開始搜索 * * @param text 傳入輸入框的文本 */ void onSearch(String text); // /** // * 提示列表項點擊時回調方法 (提示/自動補全) // */ // void onTipsItemClick(String text); } } 搜索框主要包含兩個結構:輸入欄+彈出框(自動補全或熱門搜素推薦)。
代碼不多,實現很簡單,主要是需要給EditText(輸入框)設置點擊監聽和文本改變監聽,有以下幾點:
1. 當輸入框沒有文本時,點擊輸入框,顯示熱門搜索列表框。
2. 當輸入框有文本時,點擊輸入框,應顯示自動補全列表框。
3. 當輸入框的文本發生改變時,需要更新自動補全列表框的數據。由于這些數據應該是在外部(調用者)中獲得的,所以可以通過接口回調的形式,當需要更新時,通知監聽者更新數據。
4. 當輸入框的文本從空”“變換到非空時,即有字符時,界面應顯示自動補全框,隱藏熱門搜索框。
5. 當輸入框的文本從非空變為空時,系統應隱藏自動補全框和熱門搜索框。
6. 需要監聽是否按下search鍵(enter),按下時通知監聽者執行search操作
結合以上6點和在上文分析過的內容,就能很輕松地實現該view。
之后來看看搜索界面的布局文activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:orientation="vertical"> <com.yetwish.customsearchdemo.activity.widge.SearchView android:id="@+id/main_search_layout" android:layout_width="match_parent" android:layout_height="wrap_content"> </com.yetwish.customsearchdemo.activity.widge.SearchView> <ListView android:visibility="gone" android:id="@+id/main_lv_search_results" android:layout_width="match_parent" android:layout_height="wrap_content"> </ListView> </LinearLayout>
就是一個SearchView加上一個結果列表,這些我們在上文都分析過了,所以也沒什么好說的。布局可根據自身需求去自定義。
最后就是搜索界面調用該view MainActiviy.java
public class MainActivity extends Activity implements SearchView.SearchViewListener { /** * 搜索結果列表view */ private ListView lvResults; /** * 搜索view */ private SearchView searchView; /** * 熱搜框列表adapter */ private ArrayAdapter<String> hintAdapter; /** * 自動補全列表adapter */ private ArrayAdapter<String> autoCompleteAdapter; /** * 搜索結果列表adapter */ private SearchAdapter resultAdapter; /** * 數據庫數據,總數據 */ private List<Bean> dbData; /** * 熱搜版數據 */ private List<String> hintData; /** * 搜索過程中自動補全數據 */ private List<String> autoCompleteData; /** * 搜索結果的數據 */ private List<Bean> resultData; /** * 默認提示框顯示項的個數 */ private static int DEFAULT_HINT_SIZE = 4; /** * 提示框顯示項的個數 */ private static int hintSize = DEFAULT_HINT_SIZE; /** * 設置提示框顯示項的個數 * * @param hintSize 提示框顯示個數 */ public static void setHintSize(int hintSize) { MainActivity.hintSize = hintSize; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); initData(); initViews(); } /** * 初始化視圖 */ private void initViews() { lvResults = (ListView) findViewById(R.id.main_lv_search_results); searchView = (SearchView) findViewById(R.id.main_search_layout); //設置監聽 searchView.setSearchViewListener(this); //設置adapter searchView.setTipsHintAdapter(hintAdapter); searchView.setAutoCompleteAdapter(autoCompleteAdapter); lvResults.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) { Toast.makeText(MainActivity.this, position + "", Toast.LENGTH_SHORT).show(); } }); } /** * 初始化數據 */ private void initData() { //從數據庫獲取數據 getDbData(); //初始化熱搜版數據 getHintData(); //初始化自動補全數據 getAutoCompleteData(null); //初始化搜索結果數據 getResultData(null); } /** * 獲取db 數據 */ private void getDbData() { int size = 100; dbData = new ArrayList<>(size); for (int i = 0; i < size; i++) { dbData.add(new Bean(R.drawable.icon, "android開發必備技能" + (i + 1), "Android自定義view——自定義搜索view", i * 20 + 2 + "")); } } /** * 獲取熱搜版data 和adapter */ private void getHintData() { hintData = new ArrayList<>(hintSize); for (int i = 1; i <= hintSize; i++) { hintData.add("熱搜版" + i + ":Android自定義View"); } hintAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, hintData); } /** * 獲取自動補全data 和adapter */ private void getAutoCompleteData(String text) { if (autoCompleteData == null) { //初始化 autoCompleteData = new ArrayList<>(hintSize); } else { // 根據text 獲取auto data autoCompleteData.clear(); for (int i = 0, count = 0; i < dbData.size() && count < hintSize; i++) { if (dbData.get(i).getTitle().contains(text.trim())) { autoCompleteData.add(dbData.get(i).getTitle()); count++; } } } if (autoCompleteAdapter == null) { autoCompleteAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, autoCompleteData); } else { autoCompleteAdapter.notifyDataSetChanged(); } } /** * 獲取搜索結果data和adapter */ private void getResultData(String text) { if (resultData == null) { // 初始化 resultData = new ArrayList<>(); } else { resultData.clear(); for (int i = 0; i < dbData.size(); i++) { if (dbData.get(i).getTitle().contains(text.trim())) { resultData.add(dbData.get(i)); } } } if (resultAdapter == null) { resultAdapter = new SearchAdapter(this, resultData, R.layout.item_bean_list); } else { resultAdapter.notifyDataSetChanged(); } } /** * 當搜索框 文本改變時 觸發的回調 ,更新自動補全數據 * @param text */ @Override public void onRefreshAutoComplete(String text) { //更新數據 getAutoCompleteData(text); } /** * 點擊搜索鍵時edit text觸發的回調 * * @param text */ @Override public void onSearch(String text) { //更新result數據 getResultData(text); lvResults.setVisibility(View.VISIBLE); //第一次獲取結果 還未配置適配器 if (lvResults.getAdapter() == null) { //獲取搜索數據 設置適配器 lvResults.setAdapter(resultAdapter); } else { //更新搜索數據 resultAdapter.notifyDataSetChanged(); } Toast.makeText(this, "完成搜素", Toast.LENGTH_SHORT).show(); } } 使用SearchView比較簡單,只要給SearchView設置onSearchViewListener監聽接口,實現對應的方法,并給SearchView傳入熱搜版和自動補全的adapter既可。
這里使用的匹配算法比較簡單,也沒有考慮多個搜索詞的情況,(這些之后都可以再完善),主要實現就是在總數據中匹配每個Bean的Title是否包含搜索詞,包含則表示該數據匹配,否則不匹配。然后將所有匹配的Bean顯示到結果列表中。
考慮到實際開發中,數據量十分龐大,可以只把結果集的一部分(如前10個)顯示出來,上拉到底的時候再加載之后的記錄,也就是可以加入上拉加載的機制,使app性能更優化。
自動補全匹配也是采用相同的算法。算法都比較簡單,當然也可以弄得復雜點,比如根據“ ”(空格)去分割輸入文本,再逐個考慮單個搜索詞的匹配項,把匹配次數從多到少排列出結果集等等。這里不細說。
這里有一個問題是進入該搜索界面時需要加載所有的數據項到內存,當數據項很多時,是否會占用大量的內存?如果是應該如何避免?是采用只加載一部分數據的形式,還是直接使用搜索詞到數據庫中查詢更優?還請各位看官大神們給出寶貴的意見~
好了,自定義搜索框到這就打造完成啦,是不是感覺簡單過頭了。
各位看官如果有任何問題可評論或者發郵件跟我聯系yetwish@gmail.com
囧~忘記貼代碼了,代碼放在github上,各位看官直接download即可
鏈接:https://github.com/yetwish/CustomSearchView
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VEVB武林網。
新聞熱點
疑難解答