利用SimpleCursorAdapter自定义SearchView实践
导语
搜索栏是咱们生活中随处可见的一个小零件,可是离了他们在解决问题时的时间复杂度就如同从nlogn上升到了np问题,下面这个就是我们最常见的一种搜索栏。
那么接下来就让我们尽可能的实现相似的效果。
数据库
首先让我们写一个SqliteOpenHelper用于操纵数据库
package edu.fjnu.mysearchview;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DBSqliteOpenHelper extends SQLiteOpenHelper {
private static String name="data.db";
private static Integer version= 1;
public DBSqliteOpenHelper(Context context){
super(context,name,null,version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("Create table records(id integer primary key autoincrement,name varchar(256),photo_id integer)");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
文字搜索栏
package edu.fjnu.mysearchview;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.appcompat.widget.AppCompatEditText;
public class ModEditText extends AppCompatEditText {
private Drawable clearDrawable;
private Drawable searchDrawable;
public ModEditText(Context context) {
super(context);
init();
}
public ModEditText(Context context, AttributeSet attrsr){
super(context,attrsr);
init();
}
public ModEditText(Context context,AttributeSet attrsr,int defStyleAttr){
super(context,attrsr,defStyleAttr);
init();
}
private void init() {
clearDrawable = getResources().getDrawable(R.drawable.delete);
searchDrawable = getResources().getDrawable(R.drawable.search);
setCompoundDrawablesWithIntrinsicBounds(searchDrawable,null,null,null);
}
//要实现输入文字时能显示一键删除图标,需要重写onTextChanged()方法与onFocusChanged()
@Override
protected void onTextChanged(CharSequence text,int start,int lengthBefore,int lengthAfter){
super.onTextChanged(text,start,lengthBefore,lengthAfter);
setClearIconVisible(hasFocus()&&text.length()>0);//当触点选中搜索框为焦点,同时EditText中文字大于0时将删除按钮设为可见
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
setClearIconVisible(focused && length()>0);//当选中为焦点时设为可见
}
//判断是否显示删除图标
private void setClearIconVisible(boolean visible) {
setCompoundDrawablesWithIntrinsicBounds(searchDrawable,null,
visible?clearDrawable:null,null);
}
//设置Touch事件
public boolean onTouchEvent(MotionEvent event){
switch (event.getAction()){
case MotionEvent.ACTION_UP://当触点松开时
Drawable drawable =clearDrawable;
if(drawable !=null && event.getX()<=(getWidth()-getPaddingRight())
&&event.getX()>=(getWidth()-getPaddingRight()-drawable.getBounds().width())){
setText("");
}
//if判断条件:触点抬起时位置坐标<=(控件的宽度-控件的右侧边缘距离[即删除图标的右侧坐标])
//同时触点抬起时位置坐标<=删除图标的左侧图标
//即触点抬起时位置坐标在删除图标时清空Text栏
break;
}
return super.onTouchEvent(event);
}
}
自定义ListView
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
由于我们需要让ListView与ScrollView结合,使列表能够滚动,为此我们需要重写ListView的onMeasure()方法
通过MeasureSpec.makeMeasureSpec(int size,int mode)方法将listview设置为无边界,同时根据布局的大小与内容的多少灵活确定Listview的高度
xml文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:focusableInTouchMode="true"
android:orientation="vertical">
<LinearLayout
android:id="@+id/search_block"
android:layout_width="match_parent"
android:layout_height="10dp"
android:orientation="horizontal"
android:paddingRight="10dp"
>
<ImageView
android:id="@+id/search_back"
android:layout_width="38dp"
android:layout_height="38dp"
android:layout_gravity="center_vertical"
android:padding="10dp"
android:src="@drawable/back" />
<edu.fjnu.mysearchview.ModEditText
android:id="@+id/et_search"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="264"
android:background="@null"
android:drawableLeft="@drawable/search"
android:drawablePadding="8dp"
android:gravity="start|center_vertical"
android:imeOptions="actionSearch"
android:singleLine="true"
/>
</LinearLayout>
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<edu.fjnu.mysearchview.SearchListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</edu.fjnu.mysearchview.SearchListView>
<TextView
android:id="@+id/tv_clear"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#F6F6F6"
android:gravity="center"
android:visibility="invisible"
android:text="清除搜索历史" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_alignParentLeft="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/ItemImage"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="20dp"
android:layout_toRightOf="@id/ItemImage"
android:id="@+id/ItemTitle"/>
</RelativeLayout>
接口
//搜索接口
public interface sCallBack{
void SearchAciton(String string);
}
//返回|退回接口
public interface bCallBack{
void BackAction(String string);
}
SearchView
package edu.fjnu.mysearchview;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.CursorAdapter;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
public class SearchView extends LinearLayout {
private Context context;
//初始化搜索框组件
private EditText editTextSearch;
private TextView textView_Clear;
private LinearLayout search_block;
private ImageView searchBack;
//初始化自定义ListView与adapter
private SearchListView listView;
private SimpleCursorAdapter adapter;//SimpleCursorAdapter是个很灵活的适配器,能灵活的将数据库的搜索结果作为数据源加入ListView,详见接下来的内容
//初始化数据库
private DBSqliteOpenHelper helper;
private SQLiteDatabase db;
//初始化搜索接口与返回接口
private sCallBack sCallBack;
private bCallBack bCallBack;
// 搜索字体属性设置:大小、颜色 & 默认提示
private Float textSizeSearch;
private int textColorSearch;
private String textHintSearch;
// 搜索框设置:高度 & 颜色&历史结果图标
private int searchBlockHeight;
private int searchBlockColor;
private int historyimg;
/**
* 构造函数
* 作用:对搜索框进行初始化
*/
public SearchView(Context context) {
super(context);
this.context = context;
initData(context);// ->>初始化数据
init();// ->>初始化设置
}
public SearchView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
initAttrs(context, attrs); // ->>初始化自定义属性
initData(context);// ->>初始化数据
init();// ->>初始化设置
}
public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
initAttrs(context, attrs); // ->>初始化自定义属性
initData(context);// ->>初始化数据
init();// ->>初始化设置
}
/**
* 作用:初始化自定义属性
*/
private void initAttrs(Context context, AttributeSet attrs) {
// 控件资源名称
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Search_View);
// 搜索框字体大小(dp)
textSizeSearch = typedArray.getDimension(R.styleable.Search_View_textSizeSearch, 20);
// 搜索框字体颜色
int defaultColor = context.getResources().getColor(R.color.colorText); // 默认颜色 = 灰色
textColorSearch = typedArray.getColor(R.styleable.Search_View_textColorSearch, defaultColor);
// 搜索框提示内容(String)
textHintSearch = typedArray.getString(R.styleable.Search_View_textHintSearch);
// 搜索框高度
searchBlockHeight = typedArray.getInteger(R.styleable.Search_View_searchBlockHeight, 150);
// 搜索框颜色
int defaultColor2 = context.getResources().getColor(R.color.colorDefault);
searchBlockColor = typedArray.getColor(R.styleable.Search_View_searchBlockColor, defaultColor2);
//设置历史记录图标
historyimg = R.mipmap.history_32px;
// 释放资源
typedArray.recycle();
}
/**
* 初始化数据库数据
*/
public void initData(Context context) {
helper = new DBSqliteOpenHelper(context);
db = helper.getWritableDatabase();
String sql="insert into records(name,photo_id) values(?,?)";
db.execSQL(sql,new Object[]{"C语言程序设计",historyimg});
db.execSQL(sql,new Object[]{"算法第四版",historyimg});
db.execSQL(sql,new Object[]{"Android实战手册",historyimg});
db.execSQL(sql,new Object[]{"Java程序与设计",historyimg});
db.execSQL(sql,new Object[]{"数据结构与算法",historyimg});
db.close();
}
/**
*
* 作用:初始化搜索框
*/
private void init() {
// 1. 初始化UI组件
initView();
// 2. 实例化数据库对象
helper = new DBSqliteOpenHelper(context);
// 3. 第1次进入时查询所有的历史搜索记录
queryData("");
/**
* "清空搜索历史"按钮
*/
textView_Clear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 清空数据库
deleteData();
// 模糊搜索空字符 = 显示当前所有的搜索历史(此时是没有搜索记录的)
queryData("");
}
});
/**
* 监听输入键盘更换后的搜索按键
* 调用时刻:点击键盘上的搜索键时
*/
editTextSearch.setOnKeyListener(new View.OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
// 1. 点击搜索按键后,根据输入的搜索字段进行查询
if (!(sCallBack == null)) {
sCallBack.SearchAction(editTextSearch.getText().toString());
}
Toast.makeText(context, "你搜索的是" + editTextSearch.getText(), Toast.LENGTH_SHORT).show();
// 2. 点击搜索键后,对该搜索字段在数据库是否存在进行检查(查询)->> 关注1
boolean hasData = hasData(editTextSearch.getText().toString().trim());
// 3. 若存在,则不保存;若不存在,则将该搜索字段保存(插入)到数据库,并作为历史搜索记录
if (!hasData) {
//获取搜索栏中的文字并去除两侧的符号,作为records的name属性传入insertData方法
insertData(editTextSearch.getText().toString().trim());
queryData("");
}
}
return false;
}
});
/**
* 搜索框的文本变化监听
*/
editTextSearch.addTextChangedListener(new TextWatcher() {
private Editable s;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
// 输入文本后调用该方法
@Override
public void afterTextChanged(Editable s) {
// 每次输入后,模糊查询数据库 & 显示
// 注:若搜索框为空,则模糊搜索空字符 = 显示所有的搜索历史
String tempName = editTextSearch.getText().toString();
queryData(tempName); // ->>关注1
}
});
/**
* 搜索记录列表(ListView)监听
* 即当用户点击搜索历史里的字段后,会直接将结果当作搜索字段进行搜索
*/
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// 获取用户点击列表里的文字,并自动填充到搜索框内
TextView textView = (TextView) view.findViewById(R.id.ItemTitle);
String name = textView.getText().toString();
editTextSearch.setText(name);
Toast.makeText(context, name, Toast.LENGTH_SHORT).show();
}
});
/**
* 点击返回按键后的事件
*/
searchBack.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!(bCallBack == null)) {
bCallBack.BackAction();
}
//根据输入的内容模糊查询item,并跳转到另一个界面
Toast.makeText(context, "返回到上一页", Toast.LENGTH_SHORT).show();
}
});
}
/**
* 绑定搜索框xml视图
*/
private void initView() {
// 1. 绑定R.layout.search_layout作为搜索框的xml文件
LayoutInflater.from(context).inflate(R.layout.search_layout, this);
// 2. 绑定搜索框EditText
editTextSearch = (EditText) findViewById(R.id.et_search);
editTextSearch.setTextSize(textSizeSearch);
editTextSearch.setTextColor(textColorSearch);
editTextSearch.setHint(textHintSearch);
// 3. 搜索框背景颜色
search_block = (LinearLayout) findViewById(R.id.search_block);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) search_block.getLayoutParams();
params.height = searchBlockHeight;
search_block.setBackgroundColor(searchBlockColor);
search_block.setLayoutParams(params);
// 4. 历史搜索记录 = ListView显示
listView = (SearchListView) findViewById(R.id.listView);
// 5. 删除历史搜索记录 按钮
textView_Clear = (TextView) findViewById(R.id.tv_clear);
textView_Clear.setVisibility(INVISIBLE);
// 6. 返回按键
searchBack = (ImageView) findViewById(R.id.search_back);
}
/**
* 模糊查询数据 & 显示到ListView列表上
*/
private void queryData(String tempName) {
// 1. 模糊搜索
Cursor cursor = helper.getReadableDatabase().rawQuery(
"select id as _id,name,photo_id,deletebtn from records where name like '%" + tempName + "%' order by id desc ", null);
// 2. 创建adapter适配器对象 & 装入模糊搜索的结果
String[] from = new String[]{"name", "photo_id"};
int[] to = new int[]{R.id.ItemTitle, R.id.ItemImage};
adapter = new SimpleCursorAdapter(context, R.layout.item, cursor, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
// 3. 设置适配器
listView.setAdapter(adapter);
adapter.notifyDataSetChanged();
System.out.println(cursor.getCount());
// 当输入框为空 & 数据库中有搜索记录时,显示 "删除搜索记录"按钮
if (tempName.equals("") && cursor.getCount() != 0) {
textView_Clear.setVisibility(VISIBLE);
} else {
textView_Clear.setVisibility(INVISIBLE);
}
}
/**
* 清空数据库
*/
private void deleteData() {
db = helper.getWritableDatabase();
db.execSQL("delete from records");
db.close();
textView_Clear.setVisibility(INVISIBLE);
}
/**
*
* 检查数据库中是否已经有该搜索记录
*/
private boolean hasData(String tempName) {
// 从数据库中Record表里找到name=tempName的id
Cursor cursor = helper.getReadableDatabase().rawQuery(
"select id as _id,name,photo_id from records where name =?", new String[]{tempName});
// 判断是否有下一项数据
return cursor.moveToNext();
}
/**
* 插入数据到数据库,即写入搜索字段到历史搜索记录
*/
private void insertData(String tempName) {
db = helper.getWritableDatabase();
db.execSQL("insert into records(name,photo_id) values(?,?)",new Object[]{tempName,historyimg});
db.close();
}
}
SimpleCursorAdapter
我们可以看到SimpleCursorAdapter继承于灵活可拓展的BaseAdapter的子类CursorAdapter;
其利用Cursor作为数据来源,为ListView与数据库搭起了一条简易灵活的通道
context:Context :正在运行的,与LsitView相关联的上下文
layout:int :要绑定到ListView的布局资源文件,用于定义其中的item,to数组中的属性要与布局文件中的属性名称保持一致
c:Cursor :数据库游标,是数据的来源,当信息尚未准备好时可以为null
from:String[] :要同UI界面绑定的信息的列名,当信息尚未准备好时可以为null
to:int[] :要与from数组中列数据绑定的视图id数组,这些元素应都为TextView,当信息尚未准备好时可以为null
flags:int :决定适配器初始化时的动作 FLAG_REGISTER_CONTENT_OBSERVER.
FLAG_REGISTER_CONTENT_OBSERVER:设置后,适配器将在光标上注册一个内容观察器,并在收到通知时调用onContentChanged()。
onContentChanged:默认在收到内容变化提醒时会自动执行重新搜索逻辑,可被重写
Cursor cursor = helper.getReadableDatabase().rawQuery(
"select id as _id,name,photo_id,deletebtn from records where name like '%" + tempName + "%' order by id desc ", null);
// 2. 创建adapter适配器对象 & 装入模糊搜索的结果
String[] from = new String[]{"name", "photo_id","deletebtn"};
int[] to = new int[]{R.id.ItemTitle, R.id.ItemImage};
adapter = new SimpleCursorAdapter(context, R.layout.item, cursor, from, to, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
效果图
关注公众号:Android老皮
解锁 《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版
内容如下:
1.Android车载应用开发系统学习指南(附项目实战)
2.Android Framework学习指南,助力成为系统级开发高手
3.2023最新Android中高级面试题汇总+解析,告别零offer
4.企业级Android音视频开发学习路线+项目实战(附源码)
5.Android Jetpack从入门到精通,构建高质量UI界面
6.Flutter技术解析与实战,跨平台首要之选
7.Kotlin从入门到实战,全方面提升架构基础
8.高级Android插件化与组件化(含实战教程和源码)
9.Android 性能优化实战+360°全方面性能调优
10.Android零基础入门到精通,高手进阶之路
敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔