利用SimpleCursorAdapter自定义SearchView实践

314 阅读8分钟

利用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零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔