Android學習 實現帶自動補全與歷史記錄的自定義搜索框

帶自動補全與歷史記錄的自定義搜索框

車票列表以及車票詳情頁的實現請參照博客:Android學習 UI模仿練習之“巴士管家”選取車票
本篇博客就不再贅述

一、界面效果

效果1
效果2

二、設計實現

(一)需求分析
  1. 搜索界面 包括搜索框與歷史記錄的顯示
  2. 搜索結果界面 一個搜索結果的列表
  3. 搜索內容可以自動補全
  4. 點擊歷史記錄可直接獲取搜索結果
  5. 以車票爲例,可在三個維度(出發地,目的地,巴士類型)進行搜索,僅做簡單的搜索展示
(二)文件列表

在這裏插入圖片描述

(三)完整代碼獲取

Android學習 實現帶自動補全與歷史記錄的自定義搜索框

(四)關鍵代碼講解

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 自動補全文本框

記錄: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 綁定數據集合等相關操作

FlexboxLayout學習

/**********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中,如有錯誤請批評指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章