Android
開發中,相似下圖的搜索功能很是常見Android
自定義搜索框 開源庫,但願大家會喜歡。已在
Github
開源:地址:SearchView,歡迎Star
!java
一款封裝了 歷史搜索記錄功能 & 樣式 的Android
自定義搜索框android
已在
Github
開源:地址:SearchView,歡迎Star
!git
coding
前, 理解好用戶的需求場景 有助於咱們更好地設計 & 實現功能根據場景,梳理出來的功能業務流程圖以下:github
根據功能的業務流程圖,得出功能需求以下數據庫
下面,將根據功能需求給出特定的技術解決方案bash
先下載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 | 搜索框的佈局 |
下面將給出詳細的功能邏輯app
注:關鍵字搜索功能是因人而異的,因此本源碼僅留出接口供開發者實現,不做具體實現ide
分析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;
}
複製代碼
分析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);
};
}
複製代碼
/**
* 步驟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) {
// 步驟一、二、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);
};
}
複製代碼
/**
* 監聽輸入鍵盤更換後的搜索按鍵
* 調用時刻:點擊鍵盤上的搜索鍵時
*/
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();
}
複製代碼
SearchView
控件,具體請看:貢獻說明Star
!SearchView
控件已在
Github
上開源:SearchView,歡迎Star
!
View
實例講解,有興趣能夠繼續關注Carson_Ho的安卓開發筆記更多簡單好用的開源庫:簡單 & 好用的開源組件: