分析
根据上面这张图,简单分析一下自定义搜索框的结构与功能,有
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();
- }
- }