Android開源實戰:手把手教你實現一個簡單 & 好用的搜尋框(含歷史搜尋記錄)

Carson_Ho發表於2018-01-29

Android開源實戰:手把手教你實現一個簡單 & 好用的搜尋框(含歷史搜尋記錄)


前言

  • Android開發中,類似下圖的搜尋功能非常常見

搜尋功能

  • 今天,我將手把手教大家實現一款 封裝了 歷史搜尋記錄功能 & 樣式Android 自定義搜尋框 開源庫,希望你們會喜歡。

示意圖

已在Github開源:地址:SearchView,歡迎 Star


目錄

示意圖


1. 簡介

一款封裝了 歷史搜尋記錄功能 & 樣式Android自定義搜尋框

已在Github開源:地址:SearchView,歡迎 Star

示意圖


2. 需求場景

  • 在開始coding前, 理解好使用者的需求場景 有助於我們更好地設計 & 實現功能
  • 需求場景如下

示意圖


3. 業務流程圖

根據場景,梳理出來的功能業務流程圖如下:

示意圖


4. 功能需求

根據功能的業務流程圖,得出功能需求如下

4.1 功能列表

示意圖

4.2 功能原型圖

示意圖

4.3 示意圖

示意圖


5. 總體設計

下面,將根據功能需求給出特定的技術解決方案

5.1 總體解決方案

示意圖

5.2 專案結構說明

  • 專案工程示意圖

示意圖

先下載Demo再閱讀,效果會更好:Carson_Ho的Github地址:Search_Layout

  • 結構說明
檔案型別 作用
SearchView.java 搜尋框所有功能的實現
RecordSQLiteOpenHelper.java 建立、管理資料庫 & 版本控制
EditText_Clear.java 自定義EdiText,豐富了自定義樣式 & 一鍵刪除
ICallBack.java 點選搜尋按鍵後的介面回撥方法
bCallBack.java 點選返回按鍵後的介面回撥方法
SearchListView.java 解決ListView & ScrollView的巢狀衝突
search_layout.xml 搜尋框的佈局

6. 功能詳細設計

下面將給出詳細的功能邏輯

6.1 關鍵字搜尋

  • 描述:根據使用者輸入的搜尋欄位進行結果搜尋
  • 原型圖

注:關鍵字搜尋功能是因人而異的,所以本原始碼僅留出介面供開發者實現,不作具體實現

示意圖

  • 原始碼分析

分析1:EditText_Clear.java

  • 作用:自定義EdiText,與系統自帶的EdiText對比:多了左側圖片 & 右側圖片設定、一鍵清空EdiText內容功能
  • 具體程式碼如下:
public class EditText_Clear extends android.support.v7.widget.AppCompatEditText {
    /**
     * 步驟1:定義左側搜尋圖示 & 一鍵刪除圖示
     */
    private Drawable clearDrawable,searchDrawable;

    public EditText_Clear(Context context) {
        super(context);
        init();
        // 初始化該元件時,對EditText_Clear進行初始化 ->>步驟2
    }

    public EditText_Clear(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public EditText_Clear(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    /**
     * 步驟2:初始化 圖示資源
     */
    private void init() {
        clearDrawable = getResources().getDrawable(R.drawable.delete);
        searchDrawable = getResources().getDrawable(R.drawable.search);

        setCompoundDrawablesWithIntrinsicBounds(searchDrawable, null,
                null, null);
        // setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom)介紹
        // 作用:在EditText上、下、左、右設定圖示(相當於android:drawableLeft=""  android:drawableRight="")
        // 注1:setCompoundDrawablesWithIntrinsicBounds()傳入的Drawable的寬高=固有寬高(自動通過getIntrinsicWidth()& getIntrinsicHeight()獲取)
        // 注2:若不想在某個地方顯示,則設定為null
        // 此處設定了左側搜尋圖示

        // 另外一個相似的方法:setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)介紹
        // 與setCompoundDrawablesWithIntrinsicBounds()的區別:可設定圖示大小
          // 傳入的Drawable物件必須已經setBounds(x,y,width,height),即必須設定過初始位置、寬和高等資訊
          // x:元件在容器X軸上的起點 y:元件在容器Y軸上的起點 width:元件的長度 height:元件的高度
    }


    /**
     * 步驟3:通過監聽複寫EditText本身的方法來確定是否顯示刪除圖示
     * 監聽方法: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);
        // hasFocus()返回是否獲得EditTEXT的焦點,即是否選中
        // setClearIconVisible() = 根據傳入的是否選中 & 是否有輸入來判斷是否顯示刪除圖示->>關注1
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);
        setClearIconVisible(focused && length() > 0);
        // focused = 是否獲得焦點
        // 同樣根據setClearIconVisible()判斷是否要顯示刪除圖示
    }

    /**
     * 關注1
     * 作用:判斷是否顯示刪除圖示
     */
    private void setClearIconVisible(boolean visible) {
        setCompoundDrawablesWithIntrinsicBounds(searchDrawable, null,
                visible ? clearDrawable : null, null);
    }

    /**
     * 步驟4:對刪除圖示區域設定點選事件,即"點選 = 清空搜尋框內容"
     * 原理:當手指抬起的位置在刪除圖示的區域,即視為點選了刪除圖示 = 清空搜尋框內容
     */
    @Override
    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("");
                }
                // 判斷條件說明
                // event.getX() :抬起時的位置座標
                // getWidth():控制元件的寬度
                // getPaddingRight():刪除圖示圖示右邊緣至EditText控制元件右邊緣的距離
                // 即:getWidth() - getPaddingRight() = 刪除圖示的右邊緣座標 = X1
                // getWidth() - getPaddingRight() - drawable.getBounds().width() = 刪除圖示左邊緣的座標 = X2
                // 所以X1與X2之間的區域 = 刪除圖示的區域
                // 當手指抬起的位置在刪除圖示的區域(X2=<event.getX() <=X1),即視為點選了刪除圖示 = 清空搜尋框內容
                // 具體示意圖請看下圖
                break;
        }
        return super.onTouchEvent(event);
    }


}
複製程式碼

示意圖

對於含有一鍵清空功能 & 更多自定義樣式的EditText自定義控制元件具體請看我的另外一個簡單 & 好用的開源元件:Android自定義EditText:手把手教你做一款含一鍵刪除&自定義樣式的SuperEditText

分析2:SearchListView.java

  • 作用:解決 ListView & ScrollView 的巢狀衝突
  • 具體程式碼如下:
public class Search_Listview extends ListView {
    public Search_Listview(Context context) {
        super(context);
    }

    public Search_Listview(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Search_Listview(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    // 通過複寫其onMeasure方法,達到對ScrollView適配的效果
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

}
複製程式碼

分析3: search_layout.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:layout_width="38dp"
            android:layout_height="38dp"
            android:layout_gravity="center_vertical"
            android:padding="10dp"
            android:src="@drawable/back" />

        // 搜尋框(採用上面寫的自定義EditText
        <scut.carson_ho.searchview.EditText_Clear
            android:id="@+id/et_search"
            android:layout_width="0dp"
            android:layout_height="fill_parent"
            android:layout_weight="264"
            android:background="@null"
            android:drawablePadding="8dp"
            android:gravity="start|center_vertical"
            android:imeOptions="actionSearch"
            android:singleLine="true"
            // 最後2行 = 更換輸入鍵盤按鈕:換行 ->>搜尋
            />
        
    </LinearLayout>

    // 下方搜尋記錄佈局 = ScrollView+Listview
    <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">

                // Listview佈局(採用上述講解的SearchListView,解決了與ScrollView的衝突)
                <scut.carson_ho.searchview.SearchListView
                    android:id="@+id/listView"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">

                </scut.carson_ho.searchview.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>

複製程式碼

分析4:ICallBack.java、bCallBack.java

  • 作用:搜尋按鍵、返回按鍵回撥介面
  • 具體程式碼如下:
/**
 * ICallBack.java
 */
public interface ICallBack {
    void SearchAciton(String string);

}

/**
 * bCallBack.java
 */
public interface bCallBack {
    void BackAciton();
}
複製程式碼

分析5:SearchView.java

  • 作用:涵蓋搜尋框中所有功能,此處主要講解 關鍵字搜尋 功能實現
  • 具體程式碼如下:

/**
   * 步驟1:初始化成員變數
   */

    // 搜尋框元件
    private EditText et_search; // 搜尋按鍵
    private LinearLayout search_block; // 搜尋框佈局
    private ImageView searchBack; // 返回按鍵

    // 回撥介面
    private  ICallBack mCallBack;// 搜尋按鍵回撥介面
    private  bCallBack bCallBack; // 返回按鍵回撥介面

    // ListView列表 & 介面卡
    private SearchListView listView;
    private BaseAdapter adapter;

   /**
     * 步驟2:繫結 搜尋框 元件
     */
    
    private void initView(){
        
        // 1. 繫結R.layout.search_layout作為搜尋框的xml檔案
        LayoutInflater.from(context).inflate(R.layout.search_layout,this);
        
        // 2. 繫結搜尋框EditText
        et_search = (EditText) findViewById(R.id.et_search);

        // 3. 搜尋框背景顏色
        search_block = (LinearLayout)findViewById(R.id.search_block);
   
        // 4. 歷史搜尋記錄 = ListView顯示
        listView = (Search_Listview) findViewById(R.id.listView);
        
        // 5. 刪除歷史搜尋記錄 按鈕
        tv_clear = (TextView) findViewById(R.id.tv_clear);
        tv_clear.setVisibility(INVISIBLE); // 初始狀態 = 不可見
        
    }

/**
    * 步驟3
    * 監聽輸入鍵盤更換後的搜尋按鍵
    * 呼叫時刻:點選鍵盤上的搜尋鍵時
    */
        et_search.setOnKeyListener(new View.OnKeyListener() {
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
                    
                    // 點選搜尋按鍵後,根據輸入的搜尋欄位進行查詢
                    // 注:由於此處需求會根據自身情況不同而不同,所以具體邏輯由開發者自己實現,此處僅留出介面
                    if (!(mCallBack == null)){
                        mCallBack.SearchAciton(et_search.getText().toString());
                    }
                    Toast.makeText(context, "需要搜尋的是" + et_search.getText(), Toast.LENGTH_SHORT).show();
                    
                   
                }
                return false;
            }
        });

 /**
     * 步驟4:回撥介面
     */

// 搜尋按鍵回撥介面
public interface ICallBack {
    void SearchAciton(String string);
}

// 返回按鍵介面回撥
    public void setOnClickBack(bCallBack bCallBack){
        this.bCallBack = bCallBack;

    }
複製程式碼

6.2 實時顯示歷史搜尋記錄

  • 描述:包括 最近搜尋記錄 & 相似搜尋記錄
  • 原型圖

示意圖

  • 原始碼分析

分析1:RccordSQLiteOpenHelper.java

  • 作用:建立、管理資料庫 & 版本控制

該資料庫用於儲存使用者的搜尋歷史記錄

  • 具體程式碼如下:

對於Android SQLlite資料庫的操作請看文章:Android:SQLlite資料庫操作最詳細解析


// 繼承自SQLiteOpenHelper資料庫類的子類
public class RecordSQLiteOpenHelper extends SQLiteOpenHelper {

    private static String name = "temp.db";
    private static Integer version = 1;

    public RecordSQLiteOpenHelper(Context context) {
        super(context, name, null, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // 開啟資料庫 & 建立一個名為records的表,裡面只有一列name來儲存歷史記錄:
        db.execSQL("create table records(id integer primary key autoincrement,name varchar(200))");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

複製程式碼

分析2:SearchView.java

  • 作用:涵蓋搜尋框中所有功能,此處主要講解 實時顯示歷史搜尋記錄 功能實現
  • 具體程式碼如下:
   /**
     * 步驟1:初始化變數
     */
    // 用於存放歷史搜尋記錄
    private RecordSQLiteOpenHelper helper ;
    private SQLiteDatabase db;

    // ListView列表 & 介面卡
    private SearchListView listView;
    listView = (SearchListView) findViewById(R.id.listView);
    private BaseAdapter adapter;

    // 例項化資料庫SQLiteOpenHelper子類物件
    helper = new RecordSQLiteOpenHelper(context);

/**
  * 步驟2:搜尋框的文字變化實時監聽
  */
        et_search.addTextChangedListener(new TextWatcher() {
            @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 = et_search.getText().toString();
                queryData(tempName); // ->>關注1

            }
        });

/**
   * 步驟3:搜尋記錄列表(ListView)監聽
   * 即當使用者點選搜尋歷史裡的欄位後,會直接將結果當作搜尋欄位進行搜尋
   */
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

                // 獲取使用者點選列表裡的文字,並自動填充到搜尋框內
                TextView textView = (TextView) view.findViewById(android.R.id.text1);
                String name = textView.getText().toString();
                et_search.setText(name);
                Toast.makeText(context, name, Toast.LENGTH_SHORT).show();
            }
        });


/**
   * 關注1
   * 模糊查詢資料 & 顯示到ListView列表上
   */
    private void queryData(String tempName) {

        // 1. 模糊搜尋
        Cursor cursor = helper.getReadableDatabase().rawQuery(
                "select id as _id,name from records where name like '%" + tempName + "%' order by id desc ", null);
        // 2. 建立adapter介面卡物件 & 裝入模糊搜尋的結果
        adapter = new SimpleCursorAdapter(context, android.R.layout.simple_list_item_1, cursor, new String[] { "name" },
                new int[] { android.R.id.text1 }, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        // 3. 設定介面卡
        listView.setAdapter(adapter);
        adapter.notifyDataSetChanged();
        
        System.out.println(cursor.getCount());
        // 當輸入框為空 & 資料庫中有搜尋記錄時,顯示 "刪除搜尋記錄"按鈕
        if (tempName.equals("") && cursor.getCount() != 0){
            tv_clear.setVisibility(VISIBLE);
        }
        else {
            tv_clear.setVisibility(INVISIBLE);
        };

    }
複製程式碼

6.3 刪除歷史搜尋記錄

  • 描述:清空所有歷史搜尋記錄
  • 原型圖

示意圖

  • 原始碼分析
  /**
     * 步驟1:初始化變數
     */
      private TextView tv_clear;  // 刪除搜尋記錄按鍵
      tv_clear = (TextView) findViewById(R.id.tv_clear);
      tv_clear.setVisibility(INVISIBLE);// 初始狀態 = 不可見

/**
  * 步驟2:設定"清空搜尋歷史"按鈕
  */
        tv_clear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                // 清空資料庫->>關注2
                deleteData();
                // 模糊搜尋空字元 = 顯示所有的搜尋歷史(此時是沒有搜尋記錄的) & 顯示該按鈕的條件->>關注3
                queryData("");
            }
        });

/**
  * 關注2:清空資料庫
  */
    private void deleteData() {

        db = helper.getWritableDatabase();
        db.execSQL("delete from records");
        db.close();
        tv_clear.setVisibility(INVISIBLE);
    }

 /**
     * 關注3
     * 模糊查詢資料、顯示到ListView列表上 & 確定顯示 “刪除歷史按鈕”條件
     */
    private void queryData(String tempName) {
        // 步驟1、2、3上面已經提到了,直接看步驟4
        // 1. 模糊搜尋
        Cursor cursor = helper.getReadableDatabase().rawQuery(
                "select id as _id,name from records where name like '%" + tempName + "%' order by id desc ", null);
        // 2. 建立adapter介面卡物件 & 裝入模糊搜尋的結果
        adapter = new SimpleCursorAdapter(context, android.R.layout.simple_list_item_1, cursor, new String[] { "name" },
                new int[] { android.R.id.text1 }, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
        // 3. 設定介面卡
        listView.setAdapter(adapter);
        adapter.notifyDataSetChanged();

        // 4. 當輸入框為空 & 資料庫中有搜尋記錄時,才顯示 "刪除搜尋記錄"按鈕
        if (tempName.equals("") && cursor.getCount() != 0){
            tv_clear.setVisibility(VISIBLE);
        }
        else {
            tv_clear.setVisibility(INVISIBLE);
        };

    }
複製程式碼

6.4 儲存歷史搜尋記錄

  • 描述:將使用者輸入的搜尋欄位儲存到資料庫中
  • 原型圖

示意圖

  • 原始碼分析
/**
  * 監聽輸入鍵盤更換後的搜尋按鍵
  * 呼叫時刻:點選鍵盤上的搜尋鍵時
  */
        et_search.setOnKeyListener(new View.OnKeyListener() {
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_DOWN) {
                    // 步驟1已經講解過,直接看步驟2

                    // 1. 點選搜尋按鍵後,根據輸入的搜尋欄位進行查詢
                    // 注:由於此處需求會根據自身情況不同而不同,所以具體邏輯由開發者自己實現,此處僅留出介面
                    if (!(mCallBack == null)){
                     mCallBack.SearchAciton(et_search.getText().toString());
                    }
                    Toast.makeText(context, "需要搜尋的是" + et_search.getText(), Toast.LENGTH_SHORT).show();

                    // 2. 點選搜尋鍵後,對該搜尋欄位在資料庫是否存在進行檢查(查詢)->> 關注3
                    boolean hasData = hasData(et_search.getText().toString().trim());
                    // 3. 若存在,則不儲存;若不存在,則將該搜尋欄位儲存(插入)到資料庫,並作為歷史搜尋記錄
                    if (!hasData) {
                        insertData(et_search.getText().toString().trim()); // ->>關注4
                        queryData("");
                    }
                }
                return false;
            }
        });

/**
  * 關注3
  * 檢查資料庫中是否已經有該搜尋記錄
  */
    private boolean hasData(String tempName) {
        // 從資料庫中Record表裡找到name=tempName的id
        Cursor cursor = helper.getReadableDatabase().rawQuery(
                "select id as _id,name from records where name =?", new String[]{tempName});
        //  判斷是否有下一個
        return cursor.moveToNext();
    }

/**
  * 關注4
  * 插入資料到資料庫,即寫入搜尋欄位到歷史搜尋記錄
  */
    private void insertData(String tempName) {
        db = helper.getWritableDatabase();
        db.execSQL("insert into records(name) values('" + tempName + "')");
        db.close();
    }
複製程式碼

7. 具體使用

示意圖


8. 貢獻程式碼

  • 希望你們能和我一起完善這款簡單 & 好用的SearchView控制元件,具體請看:貢獻說明
  • 關於該開源專案的意見 & 建議可在Issue上提出。歡迎 Star

9. 總結

  • 相信你一定會喜歡上 這款簡單 & 好用的SearchView控制元件

已在Github上開源:SearchView,歡迎 Star

更多簡單好用的開源庫:簡單 & 好用的開源元件:

  1. 自定義EditText:手把手教你做一款含一鍵刪除&自定義樣式的SuperEditText
  2. 你也可以自己寫一個可愛 & 小資風格的Android載入等待自定義View

請點贊!因為你的鼓勵是我寫作的最大動力!

相關文章