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下拉框锚点即补全下拉框在何处出现
completionThreshold2补全阈值即输入几个字符后开始补全
imeOptionsactionSearch软键盘右下角返回按钮的动作指令,此处为搜索

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中,如有错误请批评指正!

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值