Android自定义View实现搜索框(SearchView)功能

时间:2022-07-28
本文章向大家介绍Android自定义View实现搜索框(SearchView)功能,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

概述

在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

以上就是本文的全部内容,希望对大家的学习有所帮助。