帶自動補全與歷史記錄的自定義搜索框
車票列表以及車票詳情頁的實現請參照博客:Android學習 UI模仿練習之“巴士管家”選取車票
本篇博客就不再贅述
一、界面效果
二、設計實現
(一)需求分析
- 搜索界面 包括搜索框與歷史記錄的顯示
- 搜索結果界面 一個搜索結果的列表
- 搜索內容可以自動補全
- 點擊歷史記錄可直接獲取搜索結果
- 以車票爲例,可在三個維度(出發地,目的地,巴士類型)進行搜索,僅做簡單的搜索展示
(二)文件列表
(三)完整代碼獲取
(四)關鍵代碼講解
1. 車票和搜索記錄的單例實現以及數據庫的相關操作
A.)單例介紹
單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
B.)車票單例實現
/**********TicketLab**********/
public class TicketLab {
//車票單例
private static TicketLab sTicketLab;
//車票集合
private List<Ticket> mTickets;
//上下文
private Context mContext;
//提供一種訪問其唯一的對象的方式,可以直接訪問
public static TicketLab getInstance(Context context){
//該類負責創建自己的對象,同時確保只有單個對象被創建。
//如果不存在則new一個,存在了直接返回
if (sTicketLab == null){
sTicketLab = new TicketLab(context);
}
return sTicketLab;
}
private TicketLab(Context context){
mContext = context;
initTickets();
}
...
//獲取所有的車票的部分信息(出發地,目的地,巴士類型),用於自動補全
public List<String> getTicketsInfo(){
List<String> data = new ArrayList<>();
for (Ticket ticket : mTickets) {
if (!data.contains(ticket.getOriginStation())){
data.add(ticket.getOriginStation());
}
if (!data.contains(ticket.getDestinationStation())){
data.add(ticket.getDestinationStation());
}
if (!data.contains(ticket.getBusType())){
data.add(ticket.getBusType());
}
}
return data;
}
//是否爲車票信息(全匹配才判定爲是)
public boolean isTicketsInfo(String content){
for (Ticket ticket : mTickets) {
if (ticket.getOriginStation().equals(content)||ticket.getDestinationStation().equals(content)||ticket.getBusType().equals(content)){
return true;
}
}
return false;
}
C.)搜索記錄單例介紹
/**********SearchRecordLab**********/
public class SearchRecordLab {
private static SearchRecordLab sSearchRecordLab;
private List<SearchRecord> mSearchRecords;
public static SearchRecordLab get(Context context){
if (sSearchRecordLab == null){
sSearchRecordLab = new SearchRecordLab(context);
}
return sSearchRecordLab;
}
private SearchRecordLab(Context context){
mSearchRecords = new ArrayList<>();
mSearchRecords.addAll(DBHelper.getHistoryRecords());
}
public List<SearchRecord> getSearchRecords() {
return mSearchRecords;
}
//添加搜索記錄
public void addSearchRecord(String content){
//判斷是否存在該搜索記錄
boolean isExist = false;
for (SearchRecord searchRecord : mSearchRecords) {
if (searchRecord.getContent().equals(content)){
isExist = true;
}
}
//如果已經存在該搜索記錄,則不添加,否則添加
if (!isExist){
//將搜索記錄添加到數據庫
DBHelper.insertHistoryRecord(content);
SearchRecord sr = new SearchRecord();
sr.setType(0);
sr.setContent(content);
mSearchRecords.add(sr);
}
}
//清空所有的搜索記錄,包括數據庫
public void clearSearchRecords(){
DBHelper.deleteAllHistoryRecords();
mSearchRecords.clear();
}
//獲取歷史記錄的字符串集合
public List<String> getHistoryToStringList(){
List<String> strings = new ArrayList<>();
for (SearchRecord searchRecord : mSearchRecords) {
strings.add(searchRecord.getContent());
}
return strings;
}
}
D.)數據庫操作
/**********DBHelper**********/
private static SQLiteDatabase db = LitePal.getDatabase();
//獲取所有的歷史記錄
public static List<SearchRecord> getHistoryRecords(){
List<SearchRecord> records = LitePal.where("type == 0 ").find(SearchRecord.class);
return records;
}
//保存一條歷史記錄
public static void insertHistoryRecord(String content){
SearchRecord sr = new SearchRecord();
sr.setContent(content);
sr.setType(0);
sr.save();
}
//刪除所有的歷史記錄
public static void deleteAllHistoryRecords(){
LitePal.deleteAll(SearchRecord.class, "type == 0");
}
2. 搜索界面,包括搜索框與歷史記錄的顯示;搜索內容可以自動補全;點擊歷史記錄可直接獲取搜索結果
難點: 自動補全與歷史記錄的排列
思路介紹:
UI 思路: 自動補全採用 AutoCompleteTextView 實現,提示補全的下拉框爲了美觀,寫了一個 invisibile的view,讓下拉框在該 view 的下方出現;歷史記錄的排列爲放得下就放,放不下就另外起一行,採用 Flexbox 與 RecyclerView 實現。
邏輯思路: 依據搜索框內容,點擊搜索時,要完成以下四件事情:
- 1.)將搜索記錄加入下方 RecyclerView 的適配器數據集合並刷新 RecyclerView;
- 2.)將搜索記錄加入 AutoCompleteTextView 的適配器數據集合當中;
- 3.)把搜索記錄保存到數據庫;
- 4.)將搜索內容傳入新活動中,完成搜索功能。點擊歷史記錄進行搜索時,僅需要將搜索內容傳入新活動中,完成搜索功能。點擊清空圖標時,需完成以下兩件事情:(1)彈窗,提示用戶是否清除歷史記錄;(2)若用戶選擇否,則 Toast 提示取消操作;若用戶選擇是則清空 RecyclerView 的適配器數據集合並刷新 RecyclerView,清空 AutoCompleteTextView 的適配器數據集合,Toast 提示完成操作。
- 值得注意的是重複搜索的數據需要做篩選,不做二次保存。
A.)佈局實現
<?xml version="1.0" encoding="utf-8"?>
<!--**********activity_search**********-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".SearchActivity">
<LinearLayout
android:id="@+id/widget_search_ll"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp"
android:layout_marginTop="14dp"
android:layout_marginBottom="7dp"
android:background="@drawable/bg_rrc_primary_dark_filled_white">
<ImageView
android:id="@+id/widget_search_left_icon_iv"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_margin="4dp"
app:srcCompat="@drawable/icon_search" />
<AutoCompleteTextView
android:id="@+id/widget_search_ac_tv"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:dropDownAnchor="@id/widget_search_view"
android:completionThreshold="2"
android:background="@null"
android:hint="@string/search_hint"
android:textSize="14sp"
android:textColor="@color/fc_light_grey"
android:singleLine="true"
android:imeOptions="actionSearch"/>
<ImageView
android:id="@+id/widget_search_right_icon_iv"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_margin="4dp"
app:srcCompat="@drawable/icon_voice"/>
</LinearLayout>
<View
android:id="@+id/widget_search_view"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginLeft="36dp"
android:layout_marginRight="36dp"
android:visibility="invisible" />
<RelativeLayout
android:id="@+id/widget_search_rl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:layout_marginRight="14dp">
<TextView
android:id="@+id/widget_search_history_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_alignParentTop="true"
android:text="@string/search_history"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/fc_light_dark"/>
<ImageView
android:id="@+id/widget_search_empty_iv"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@id/widget_search_history_tv"
app:srcCompat="@drawable/icon_empty_bin"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widget_search_history_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/widget_search_empty_iv" />
</RelativeLayout>
</LinearLayout>
B.)AutoCompleteTextView 自動補全文本框
屬性名 | 屬性值 | 效果 |
---|---|---|
dropDownAnchor | @id/widget_search_view | 下拉框錨點即補全下拉框在何處出現 |
completionThreshold | 2 | 補全閾值即輸入幾個字符後開始補全 |
imeOptions | actionSearch | 軟鍵盤右下角返回按鈕的動作指令,此處爲搜索 |
C.)AutoCompleteTextView 綁定數據集合等相關操作
/**********SearchActivity**********/
//綁定控件
AutoCompleteTextView mAutoCompleteTextView = (AutoCompleteTextView)findViewById(R.id.widget_search_ac_tv);
//新建數組適配器用於 AutoCompleteTextView 控件 參數一:上下文 參數二:佈局樣式(此處採用android自帶的簡易佈局) 參數三:數據集合,此處爲歷史搜索記錄的字符串集合
ArrayAdapter mArrayAdapter = new ArrayAdapter(this,android.R.layout.simple_list_item_1,SearchRecordLab.get(this).getHistoryToStringList());
//添加車票相關信息的字符串集合到數組適配器中用於自動補全
mArrayAdapter.addAll(TicketLab.getInstance(this).getTicketsInfo());
//給自動補全文本框設置適配器
mAutoCompleteTextView.setAdapter(mArrayAdapter);
//AutoCompleteTextView 編輯活動監聽
mAutoCompleteTextView.setOnEditorActionListener(new AutoCompleteTextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
//若爲搜索活動,則開始執行相關邏輯
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
String content = v.getText().toString();
//搜索內容爲空,則退出,不執行邏輯
if ("".equals(content)){
return false;
}
//將搜索記錄添加到搜索記錄的單例當中,單例中關於重複值處理還有數據庫處理的邏輯,見本篇博客的上方內容
SearchRecordLab.get(SearchActivity.this).addSearchRecord(content);
//歷史記錄RecyclerView 刷新數據集合
mAdapter.notifyDataSetChanged();
//將搜索框的文本清空
v.setText("");
//若爲新的搜索內容(即非車票相關信息也不是已有的搜索記錄),則添加到補全適配器中的數據集合
if (!TicketLab.getInstance(SearchActivity.this).isTicketsInfo(content)){
mArrayAdapter.add(content);
}
//跳轉到搜索結果界面,並將搜索內容傳遞過去
Intent intent = new Intent(SearchActivity.this,SearchResultActivity.class);
intent.putExtra(SEARCH_CONTENT,content);
startActivity(intent);
return true;
}
return false;
}
});
D.)RecyclerView 綁定數據集合等相關操作
/**********SearchActivity**********/
//綁定控件
RecyclerView mHistoryRV = (RecyclerView)findViewById(R.id.widget_search_history_rv);
//設置適配器
SRAdapterForRV mAdapter = new SRAdapterForRV(SearchRecordLab.get(this).getSearchRecords());
FlexboxLayoutManager layoutManager = new FlexboxLayoutManager(this);
layoutManager.setFlexWrap(FlexWrap.WRAP); //設置是否換行
layoutManager.setAlignItems(AlignItems.STRETCH);
mHistoryRV.setLayoutManager(layoutManager);
mHistoryRV.setAdapter(mAdapter);
//歷史記錄點擊事件
mAdapter.setOnItemClickListener(new SRAdapterForRV.OnItemClickListener() {
@Override
public void onClick(SearchRecord searchRecord) {
//跳轉到搜索結果界面,並將搜索內容傳遞過去
String content = searchRecord.getContent();
Intent intent = new Intent(SearchActivity.this,SearchResultActivity.class);
intent.putExtra(SEARCH_CONTENT,content);
startActivity(intent);
}
});
E.)清空歷史記錄操作
/**********SearchActivity**********/
//綁定控件
ImageView mEmptyIV = (ImageView) findViewById(R.id.widget_search_empty_iv);
mEmptyIV.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//通過AlertDialog.Builder創建一個AlertDialog的實例
AlertDialog.Builder dialog = new AlertDialog.Builder(SearchActivity.this);
//設置對話框的標題,內容,可否取消屬性
dialog.setTitle(getResources().getString(R.string.alert_dialog_title));
dialog.setMessage(getResources().getString(R.string.alert_dialog_msg_for_search_activity));
dialog.setCancelable(true);
//調用setPositiveButton()方法爲對話框設置確定按鈕的點擊事件
dialog.setPositiveButton(getResources().getString(R.string.alert_dialog_ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//清空歷史記錄以及自動補全數據集合中的歷史記錄,並提示操作完成
SearchRecordLab.get(SearchActivity.this).clearSearchRecords();
mAdapter.notifyDataSetChanged();
mArrayAdapter.clear();
mArrayAdapter.addAll(TicketLab.getInstance(SearchActivity.this).getTicketsInfo());
Toast.makeText(SearchActivity.this,getResources().getString(R.string.alert_dialog_ok_toast),Toast.LENGTH_SHORT).show();
}
});
//調用setNegativeButton()方法爲對話框設置取消按鈕的點擊事件
dialog.setNegativeButton(getResources().getString(R.string.alert_dialog_cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
//提示操作取消
Toast.makeText(SearchActivity.this,getResources().getString(R.string.alert_dialog_cancel_toast),Toast.LENGTH_SHORT).show();
}
});
dialog.show();//將對話框顯示出來
}
});
3. 搜索功能的模擬實現
A.)獲取上一個活動傳遞過來的數據
/**********SearchResultActivity**********/
String searchContent = getIntent().getStringExtra(SEARCH_CONTENT);
B.)依據搜索內容進行搜索並獲得搜索結果
/**********TicketLab**********/
/**
*
* @param content 搜索內容
* @param type 搜索類型 0 按照出發地點搜索 1 按照目的地搜索 2 按照巴士類型搜索 3 0+1+2
* @return List<Ticket>
*/
public List<Ticket> searchResult(String content,int type){
List<Ticket> tickets = new ArrayList<>();
...
//包含匹配即添加進搜索結果列表
for (Ticket ticket : mTickets) {
if (ticket.getOriginStation().contains(content) || ticket.getDestinationStation().contains(content) || ticket.getBusType().contains(content)){
tickets.add(ticket);
}
}
...
if (tickets.size() > 0){
return tickets;
}else {
return mTickets;
}
}
/**********SearchResultActivity**********/
mTickets.addAll(TicketLab.getInstance(this).searchResult(searchContent,3));
C.)搜索結果的展示
/**********SearchResultActivity**********/
//綁定控件
RecyclerView mRecyclerView = findViewById(R.id.recycler_view);
//將搜索結果的數據集合添加進適配器,並將適配器設置給相應的 RecyclerView
LinearLayoutManager layoutManagerMR = new LinearLayoutManager(this);
layoutManagerMR.setOrientation(LinearLayoutManager.VERTICAL);//設置佈局的排列方向
mRecyclerView.setLayoutManager(layoutManagerMR);
TicketAdapterRV mAdapter = new TicketAdapterRV(mTickets,this);
mRecyclerView.setAdapter(mAdapter);
//添加分割線
mRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL));
三、心得體會
多學習,多編碼,多思考。實現後想想有沒有更好的實現方法!與君共勉,一同進步!
持續學習Android中,如有錯誤請批評指正!