Android学习 实现带自动补全与历史记录的自定义搜索框

Posted Nicholas_hzf

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android学习 实现带自动补全与历史记录的自定义搜索框相关的知识,希望对你有一定的参考价值。

带自动补全与历史记录的自定义搜索框

车票列表以及车票详情页的实现请参照博客:Android学习 UI模仿练习之“巴士管家”选取车票
本篇博客就不再赘述

一、界面效果


二、设计实现

(一)需求分析
  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<

以上是关于Android学习 实现带自动补全与历史记录的自定义搜索框的主要内容,如果未能解决你的问题,请参考以下文章

CRT 连接 ENSP时,Tab 键无法补全与回车键显示双行的解决方法

基于python实现-根据Excel表格指定的UniqueKey的顺序-到另一个参考表格中查找-补全与自己相关的数据

Android开源实战:手把手带你实现一个简单好用的搜索框(含历史搜索记录)

第十四课时之交互式输入行编辑历史回溯

Linux命令之bash的基础特性(命令历史命令补全路径补全命令行展开。)

Linux学习(二十一)Shell基础认识shell命令历史命令补全别名通配符管道符与前后台控制