好久没写博客了,主要是最近太忙了(好吧,其实是因为自己太懒了)。进入正题,今天主要实现的功能是:搜索。 没错,搜索模块在很多电商类app中是很常见的,实现起来也比较容易,但是经常在群里看见有人问有没有关于搜索功能的demo,加之我最近自己做的一个项目中也有搜索模块,所以这里就来说说我是怎么实现的。
这里非常感谢 干货集中营 提供的搜索的接口。据说不含效果图的文章都是耍流氓,,吓得我赶紧来了张效果图,如下图所示
需求介绍
1.首先,我们要监听搜索框EditText 我们在afterTextChanged()方法中需要去进行判断,当EditText有值时,搜索框后面的清除图标显示,我们根据输入的关键字向服务器发送请求(通过Handler每隔一段时间延迟请求),这样做的原因是因为 假如用户输入io然后又在极短时间内继续输入s,延迟发送请求就可以避免重复像服务器提交数据。需要注意的是,每次发送请求之前需要检查是否还存在有未处理的消息,有,则需要取消掉。
2.当我们向服务器请求到数据之后,点击item进入详情页的时候,我们需要将这条数据存储起来,作为历史记录向用户展示。关于数据存储的方式有很多,这里我采用的是GreenDao数据库来保存数据。这里特别说明一下,新版本的GreenDao使用步骤简化了很多,使用起来还是很爽的。关于GreenDao的使用,如果还没用过此数据库的童鞋可以看我这篇文章http://blog.csdn.net/xiaxiazaizai01/article/details/53118748,这里就不再详细介绍了。
3.当我们用电商类的搜索时,可以发现,最近搜索的记录会显示在最上面,并且当再次搜索此字段时,并不会重复添加,只是更新了时间,因为我们是按时间排序的。
4.主要介绍下此demo中用到的一些比较不错的框架,BaseRecyclerViewAdapterHelper 非常好用的一个Recyclerview框架,OkGo 网络请求
具体代码说明
首先,看看我们的布局文件 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ECECEC"
>
<LinearLayout
android:id="@+id/ll_top"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center_vertical"
android:background="#FFFFFF"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/search_back"
android:layout_width="45dp"
android:layout_height="match_parent"
android:paddingLeft="6dp"
android:paddingRight="6dp"
android:src="@drawable/back_ic"
/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:gravity="center_vertical"
android:background="@drawable/search_bg_shape"
android:orientation="horizontal"
>
<EditText
android:id="@+id/et_search"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:drawableLeft="@drawable/search_icon"
android:hint="android/ios/福利/前端/休息视频"
android:drawablePadding="10dp"
android:textSize="14sp"
android:layout_margin="5dp"
android:singleLine="true"
android:background="@null"
/>
<ImageView
android:id="@+id/search_clear"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_margin="5dp"
android:src="@drawable/clear_search_ic"
android:visibility="gone"/>
</LinearLayout>
<TextView
android:id="@+id/tv_search"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:text="搜索"
android:textColor="#333333"
android:textSize="16sp"/>
</LinearLayout>
<View
android:id="@+id/line"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@id/ll_top"
android:background="#e2e2e2"/>
<!-- 热搜模块,这里不做重点,省略。。。 -->
<!-- 搜索历史模块 -->
<LinearLayout
android:id="@+id/ll_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/line"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="10dp"
android:text="历史搜索"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#e2e2e2"/>
<android.support.v7.widget.RecyclerView
android:id="@+id/history_search_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
<!-- 搜索的结果展示模块 -->
<android.support.v7.widget.RecyclerView
android:id="@+id/search_data_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/line"
android:visibility="gone"/>
<RelativeLayout
android:id="@+id/rl_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/line"
android:layout_marginTop="20dp"
android:gravity="center_horizontal"
android:visibility="gone"
>
<com.json.search.CustomProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:padding="5dp"
app:search_progress_color="#10a679"
app:search_progress_width="1dp"
app:search_radius="6dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/progress_bar"
android:layout_marginLeft="5dp"
android:layout_centerVertical="true"
android:text="加载中..."/>
</RelativeLayout>
</RelativeLayout>
接着,看下我们存储数据库所需要的实体类是如何定义的
/**
* 历史搜索的实体类 保存到数据库
*/
@Entity
public class SearchHistoryBean {
@Id
private Long id;
@Property(nameInDb = "title")
private String desc;
@Property(nameInDb = "url")
private String url;
@Property(nameInDb = "time")
private long publishedAt;
@Generated(hash = 649301100)
public SearchHistoryBean(Long id, String desc, String url, long publishedAt) {
this.id = id;
this.desc = desc;
this.url = url;
this.publishedAt = publishedAt;
}
@Generated(hash = 1570282321)
public SearchHistoryBean() {
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public String getDesc() {
return this.desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public long getPublishedAt() {
return this.publishedAt;
}
public void setPublishedAt(long publishedAt) {
this.publishedAt = publishedAt;
}
}
主要逻辑是在我们的MainActivity中,接下来我们就来简单的看看EditText的监听。注释写的很详细了
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable editable) {
//EditText上有文字变动时,有未发出的搜索请求,则应取消
if(mHandler.hasMessages(SEARCH_MESSAGE)){
mHandler.removeMessages(SEARCH_MESSAGE);
}
//如果为空,直接显示搜索历史,否则,显示搜索结果
if(TextUtils.isEmpty(editable)){
searchClear.setVisibility(View.GONE);
rlProgress.setVisibility(View.GONE);//显示加载中的提示语
searchResultRv.setVisibility(View.GONE);
//需要进一步判断,因为用户如果点击的是键盘上自带的删除键的话,会导致历史搜索的布局不隐藏
if(mDatas != null && mDatas.size() > 0){
llHistory.setVisibility(View.VISIBLE);
}else{
llHistory.setVisibility(View.GONE);
}
} else {
if(lists != null && lists.size() > 0){//先判断之前的集合是否有数据,有的话则先清空,不然的话紧接着搜索,此时列表中还显示着上次的结果
lists.clear();
lists = null;
}
searchClear.setVisibility(View.VISIBLE);
rlProgress.setVisibility(View.VISIBLE);//显示加载中的提示语
searchResultRv.setVisibility(View.VISIBLE);
llHistory.setVisibility(View.GONE);
//延迟500ms开始搜索
mHandler.sendEmptyMessageDelayed(SEARCH_MESSAGE, 500);
}
}
});
将数据插入数据库,当我们用电商类的搜索时,可以发现,最近搜索的记录会显示在最上面,并且当再次搜索此字段时,并不会重复添加,只是更新了时间,因为我们是按时间排序的。
private void saveDatasToDb(int position) {
//插入数据之前先查 防止重复添加 这里查单条(一个实体)数据
SearchHistoryBean historyBeen = historyDao.queryBuilder().where(SearchHistoryBeanDao.Properties.Desc.eq(lists.get(position).getDesc())).build().unique();
if(historyBeen != null){
historyDao.delete(historyBeen);//存在,则删除此条数据
}
bean = new SearchHistoryBean(null,lists.get(position).getDesc(),
lists.get(position).getUrl(), Long.parseLong(TimeDifferentUtil.formatTimeDate(format,System.currentTimeMillis())));
//historyDao.insert(bean);
historyDao.insertOrReplace(bean);//其实这里用的是insertOrReplace 即使相同也会添加并且替换之前的
}
我们再来看看查询数据
//先判断数据库中是否有数据,有,则显示历史记录
historyDao = MyApplication.getDaoSession(MainActivity.this).getSearchHistoryBeanDao();
List<SearchHistoryBean> historyBeenLists = historyDao.queryBuilder().orderDesc(SearchHistoryBeanDao.Properties.PublishedAt).build().list();
if(historyBeenLists != null && historyBeenLists.size() > 0){
//说明数据库中含有数据
llHistory.setVisibility(View.VISIBLE);
mDatas = new ArrayList<>();
for(SearchHistoryBean bean : historyBeenLists){
String title = bean.getDesc();
SearchResultBean.ResultsBean resultBean = new SearchResultBean.ResultsBean();
resultBean.setDesc(title);
resultBean.setUrl(bean.getUrl());
mDatas.add(resultBean);
}
historyAdapter = new SearchHistoryAdapter(mDatas);
historySearchRv.setAdapter(historyAdapter);
if(mDatas != null && mDatas.size() > 0){
historyAdapter.addFooterView(historyFooter);
}else{
historyAdapter.removeFooterView(historyFooter);
}
}
下面给出完整的MainActivity的代码
public class MainActivity extends AppCompatActivity implements View.OnClickListener,BaseQuickAdapter.RequestLoadMoreListener{
public static String SEARCHURL = "http://gank.io/api/search/query/listview/category/";
private RelativeLayout rlProgress;//提示正在加载中
private ImageView ivBack;//返回按钮
private EditText etSearch;
private ImageView searchClear;//清除按钮
private TextView tvSearch;//标题栏上面的搜索按钮
private LinearLayout llHistory;//历史搜索的整体布局
private RecyclerView historySearchRv;//历史搜索记录
private SearchHistoryAdapter historyAdapter;//搜索历史的adapter
private RecyclerView searchResultRv;//搜索结果
private SearchResultAdapter resultAdapter;//搜索结果的adapter
private SwipeRefreshLayout swipeRefreshLayout;
private List<SearchResultBean.ResultsBean> lists;//数据源
private Gson gson;
private int currentPage = 1;//当前页
private int pageSize = 10;
private String format= "yyyyMMddHHmmss"; //初始化一些时间设置格式
private SearchHistoryBeanDao historyDao;//DAO
private SearchHistoryBean bean;
private List<SearchResultBean.ResultsBean> mDatas;
private View historyFooter;//添加尾部
private TextView tvClear;//清空历史记录
private static final int SEARCH_MESSAGE = 1;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case SEARCH_MESSAGE:
//网络请求数据
requestNetWorkDatas();
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化控件
iniViews();
//初始化搜索结果的adapter
resultAdapter = new SearchResultAdapter(null);
resultAdapter.isFirstOnly(false);
resultAdapter.setOnLoadMoreListener(this);
searchResultRv.setAdapter(resultAdapter);
//先判断数据库中是否有数据,有,则显示历史记录
historyDao = MyApplication.getDaoSession(MainActivity.this).getSearchHistoryBeanDao();
List<SearchHistoryBean> historyBeenLists = historyDao.queryBuilder().orderDesc(SearchHistoryBeanDao.Properties.PublishedAt).build().list();
if(historyBeenLists != null && historyBeenLists.size() > 0){
//说明数据库中含有数据
llHistory.setVisibility(View.VISIBLE);
mDatas = new ArrayList<>();
for(SearchHistoryBean bean : historyBeenLists){
String title = bean.getDesc();
SearchResultBean.ResultsBean resultBean = new SearchResultBean.ResultsBean();
resultBean.setDesc(title);
resultBean.setUrl(bean.getUrl());
mDatas.add(resultBean);
}
historyAdapter = new SearchHistoryAdapter(mDatas);
historySearchRv.setAdapter(historyAdapter);
if(mDatas != null && mDatas.size() > 0){
historyAdapter.addFooterView(historyFooter);
}else{
historyAdapter.removeFooterView(historyFooter);
}
}
}
private void iniViews() {
rlProgress = (RelativeLayout) findViewById(R.id.rl_progress);
ivBack = (ImageView) findViewById(R.id.search_back);
etSearch = (EditText) findViewById(R.id.et_search);
searchClear = (ImageView) findViewById(R.id.search_clear);
tvSearch = (TextView) findViewById(R.id.tv_search);
llHistory = (LinearLayout) findViewById(R.id.ll_history);
//当历史搜索有数据时,则加载footer布局 清空历史记录
historyFooter = LayoutInflater.from(this).inflate(R.layout.history_search_footer, null);
tvClear = (TextView) historyFooter.findViewById(R.id.clear_tv);
//清空历史记录
tvClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//清空集合
mDatas.clear();
//清空数据库 删除所有数据
historyDao.deleteAll();
//实时更新UI界面,还需要隐藏掉尾布局,以及历史搜索等字样
historyAdapter.notifyDataSetChanged();
historyFooter.setVisibility(View.GONE);
llHistory.setVisibility(View.GONE);
}
});
ivBack.setOnClickListener(this);
etSearch.setOnClickListener(this);
searchClear.setOnClickListener(this);
tvSearch.setOnClickListener(this);
historySearchRv = (RecyclerView) findViewById(R.id.history_search_recyclerview);
searchResultRv = (RecyclerView) findViewById(R.id.search_data_recyclerview);
//设置展示样式
historySearchRv.setLayoutManager(new LinearLayoutManager(MainActivity.this));
searchResultRv.setLayoutManager(new LinearLayoutManager(MainActivity.this));
etSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable editable) {
//EditText上有文字变动时,有未发出的搜索请求,则应取消
if(mHandler.hasMessages(SEARCH_MESSAGE)){
mHandler.removeMessages(SEARCH_MESSAGE);
}
//如果为空,直接显示搜索历史,否则,显示搜索结果
if(TextUtils.isEmpty(editable)){
searchClear.setVisibility(View.GONE);
rlProgress.setVisibility(View.GONE);//显示加载中的提示语
searchResultRv.setVisibility(View.GONE);
//需要进一步判断,因为用户如果点击的是键盘上自带的删除键的话,会导致历史搜索的布局不隐藏
if(mDatas != null && mDatas.size() > 0){
llHistory.setVisibility(View.VISIBLE);
}else{
llHistory.setVisibility(View.GONE);
}
} else {
if(lists != null && lists.size() > 0){//先判断之前的集合是否有数据,有的话则先清空,不然的话紧接着搜索,此时列表中还显示着上次的结果
lists.clear();
lists = null;
}
searchClear.setVisibility(View.VISIBLE);
rlProgress.setVisibility(View.VISIBLE);//显示加载中的提示语
searchResultRv.setVisibility(View.VISIBLE);
llHistory.setVisibility(View.GONE);
//延迟500ms开始搜索
mHandler.sendEmptyMessageDelayed(SEARCH_MESSAGE, 500);
}
}
});
//点击搜索结果item,跳转到相应页面,并且将item上的内容保存到greendao数据库
searchResultRv.addOnItemTouchListener(new OnItemClickListener() {
@Override
public void onSimpleItemClick(BaseQuickAdapter adapter, View view, int position) {
//先将内容填充到EditText上,再跳转
// etSearch.setText(lists.get(position).getDesc());
//跳转
Intent intent = new Intent(MainActivity.this, InfoActivity.class);
intent.putExtra("url",lists.get(position).getUrl());
startActivity(intent);
//保存到数据库
saveDatasToDb(position);
//finish
MainActivity.this.finish();
}
});
//点击历史搜索中的item 跳转到相应的页面
historySearchRv.addOnItemTouchListener(new OnItemClickListener() {
@Override
public void onSimpleItemClick(BaseQuickAdapter adapter, View view, int position) {
//先将内容填充到EditText上,再跳转
// etSearch.setText(mDatas.get(position).getDesc());
//跳转
Intent intent = new Intent(MainActivity.this, InfoActivity.class);
intent.putExtra("url",mDatas.get(position).getUrl());
startActivity(intent);
//同时将此数据更新到最上面
//插入数据之前先查 防止重复添加 这里查单条(一个实体)数据
SearchHistoryBean historyBeen = historyDao.queryBuilder().where(SearchHistoryBeanDao.Properties.Desc.eq(mDatas.get(position).getDesc())).build().unique();
if(historyBeen != null){
historyDao.delete(historyBeen);//存在,则删除此条数据
}
//historyDao = MyApplication.getDaoSession(SearchActivity.this).getSearchHistoryBeanDao();
bean = new SearchHistoryBean(null,mDatas.get(position).getDesc(),
mDatas.get(position).getUrl(), Long.parseLong(TimeDifferentUtil.formatTimeDate(format,System.currentTimeMillis())));
historyDao.insertOrReplace(bean);//其实这里用的是insertOrReplace 即使相同也会添加并且替换之前的
//finish
MainActivity.this.finish();
}
});
}
private void saveDatasToDb(int position) {
//插入数据之前先查 防止重复添加 这里查单条(一个实体)数据
SearchHistoryBean historyBeen = historyDao.queryBuilder().where(SearchHistoryBeanDao.Properties.Desc.eq(lists.get(position).getDesc())).build().unique();
if(historyBeen != null){
historyDao.delete(historyBeen);//存在,则删除此条数据
}
//historyDao = MyApplication.getDaoSession(SearchActivity.this).getSearchHistoryBeanDao();
bean = new SearchHistoryBean(null,lists.get(position).getDesc(),
lists.get(position).getUrl(), Long.parseLong(TimeDifferentUtil.formatTimeDate(format,System.currentTimeMillis())));
//historyDao.insert(bean);
historyDao.insertOrReplace(bean);//其实这里用的是insertOrReplace 即使相同也会添加并且替换之前的
}
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.search_back:
finish();
break;
case R.id.search_clear:
etSearch.setText("");
//还需要判断
if(mDatas != null && mDatas.size() > 0){
llHistory.setVisibility(View.VISIBLE);
}else{
llHistory.setVisibility(View.GONE);
}
break;
case R.id.tv_search:
break;
}
}
private void requestNetWorkDatas() {
currentPage = 1;
OkGo.get(SEARCHURL+etSearch.getText().toString().trim()+"/count/10/page/"+currentPage)
.tag("MainActivity")
.execute(new StringCallback() {
@Override
public void onSuccess(String s, Call call, Response response) {
gson = new Gson();
SearchResultBean searchResultBean = gson.fromJson(s, SearchResultBean.class);
if(!searchResultBean.isError()){
lists = searchResultBean.getResults();
if(lists != null && lists.size() > 0){
resultAdapter.setNewData(lists);
}
}
}
@Override
public void onError(Call call, Response response, Exception e) {
super.onError(call, response, e);
}
@Override
public void onBefore(BaseRequest request) {
super.onBefore(request);
//弹窗提醒正在加载中
rlProgress.setVisibility(View.VISIBLE);
}
@Override
public void onAfter(String s, Exception e) {
super.onAfter(s, e);
//关闭弹窗
rlProgress.setVisibility(View.GONE);
}
});
}
@Override
public void onLoadMoreRequested() {
currentPage++;
searchResultRv.postDelayed(new Runnable() {
@Override
public void run() {
OkGo.get(SEARCHURL+etSearch.getText().toString().trim()+"/count/10/page/"+currentPage)
.tag("MainActivity")
.execute(new StringCallback() {
@Override
public void onSuccess(String s, Call call, Response response) {
gson = new Gson();
SearchResultBean searchResultBean = gson.fromJson(s, SearchResultBean.class);
if(!searchResultBean.isError()){
List<SearchResultBean.ResultsBean> lists = searchResultBean.getResults();
if(lists != null && lists.size() > 0){
//定义接口时,我们设置的是每页显示10条数据
if(lists.size() < pageSize){
//请求获取到的总数据 < 每页需要显示的数据时,隐藏掉没有更多数据的提示
resultAdapter.loadMoreEnd(true);//true:隐藏提示 false:显示提示
}else{
// if(mCurrentCounter >= totalCounts){//由于接口获取不到总的数据量,所以这里先注释掉
// categoryThreeAdapter.loadMoreEnd(false);//刷新完成后,提示没有更多数据,false:提示 true:隐藏
// }else{
//注意,这里用的是addData(),在之前的数据集合后面添加下一条
resultAdapter.addData(lists);
// mCurrentCounter = categoryThreeAdapter.getData().size();
//调用完成的方法 注意:加载完数据之后一定要调用完成的方法,否则不会再执行上拉刷新,这里我将刷新完成放在了请求完成方法中
// categoryThreeAdapter.loadMoreComplete();
// }
}
}else{
//当上拉加载失败时,提示
resultAdapter.loadMoreFail();
}
}
}
@Override
public void onError(Call call, Response response, Exception e) {
super.onError(call, response, e);
}
@Override
public void onAfter(String s, Exception e) {
super.onAfter(s, e);
//最后调用结束刷新的方法
resultAdapter.loadMoreComplete();
}
});
}
}, 1000);
}
@Override
protected void onDestroy() {
super.onDestroy();
OkGo.getInstance().cancelTag("MainActivity");
mHandler.removeMessages(SEARCH_MESSAGE);
mHandler = null;
}
}