Android 天气APP(十五)增加城市搜索、历史搜索记录

时间:2022-07-25
本文章向大家介绍Android 天气APP(十五)增加城市搜索、历史搜索记录,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

效果图如下:

前言

看这篇文章之前,你是否是一路看过来的呢?如果你单独看着一篇的话,有些内容你可能看不懂,所以我建议你一篇一篇的看,这是第十五篇文章了,前面还有十四篇,建议先了解一下,传送门: 天气APP-专栏 在我思虑良久之后决定加一个城市的搜索功能,反正有现成的API,不用白不用,我白嫖侠是不放过任何一个可以白嫖的机会。

正文

城市搜索,我是打算新建一个Activity来专门做这个功能,继续在原来的MainActivity中写的话,就太麻烦了,可能自己看着也会觉得太多代码了,不爽,至于新开启一个页面写的话,就会有两个页面的数据交互方面的问题,这也是本文中的重点讲解对象,至于搜索的那些,都是可以轻松实现的,你说呢?闲话少说,言归正传,这自然又需要一个新的API接口了。

① 新增API接口

还记得ServiceGenerator吗?这里面要新增一个访问地址了。

一目了然吧,为了你们不用自己敲代码,我粘贴一下:

	case 2://搜索城市
        BASE_URL = "https://search.heweather.net";
        break;

然后是创建一个接收返回数据的实体bean,SearchCityResponse 代码如下:

package com.llw.goodweather.bean;

import java.util.List;

public class SearchCityResponse {

    private List<HeWeather6Bean> HeWeather6;

    public List<HeWeather6Bean> getHeWeather6() {
        return HeWeather6;
    }

    public void setHeWeather6(List<HeWeather6Bean> HeWeather6) {
        this.HeWeather6 = HeWeather6;
    }

    public static class HeWeather6Bean {
        /**
         * basic : [{"cid":"CN101010100","location":"北京","parent_city":"北京","admin_area":"北京","cnty":"中国","lat":"39.90498734","lon":"116.4052887","tz":"+8.00","type":"city"},{"cid":"CN101132101","location":"北屯","parent_city":"北屯","admin_area":"新疆","cnty":"中国","lat":"47.35317612","lon":"87.82492828","tz":"+8.00","type":"city"},{"cid":"CN101340101","location":"台北","parent_city":"台北","admin_area":"台湾","cnty":"中国","lat":"25.04000092","lon":"121.51599884","tz":"+8.00","type":"city"},{"cid":"CN101221201","location":"淮北","parent_city":"淮北","admin_area":"安徽","cnty":"中国","lat":"33.97170639","lon":"116.79466248","tz":"+8.00","type":"city"},{"cid":"CN101301301","location":"北海","parent_city":"北海","admin_area":"广西","cnty":"中国","lat":"21.4733429","lon":"109.11925507","tz":"+8.00","type":"city"},{"cid":"CN101090303","location":"张北","parent_city":"张家口","admin_area":"河北","cnty":"中国","lat":"41.15171432","lon":"114.71595001","tz":"+8.00","type":"city"},{"cid":"4A570","location":"北雅加达","parent_city":"北雅加达","admin_area":"雅加达","cnty":"印度尼西亚","lat":"-6.18638897","lon":"106.82944489","tz":"+7.00","type":"city"},{"cid":"CN101091106","location":"北戴河","parent_city":"秦皇岛","admin_area":"河北","cnty":"中国","lat":"39.82512283","lon":"119.48628235","tz":"+8.00","type":"city"},{"cid":"34272","location":"Beitou District","parent_city":"台北市","admin_area":"台湾","cnty":"中国","lat":"25.11669922","lon":"121.5","tz":"+8.00","type":"city"},{"cid":"VN1591449","location":"北宁市","parent_city":"北宁市","admin_area":"北宁省","cnty":"越南","lat":"21.18333244","lon":"106.05000305","tz":"+7.00","type":"city"}]
         * status : ok
         */

        private String status;
        private List<BasicBean> basic;

        public String getStatus() {
            return status;
        }

        public void setStatus(String status) {
            this.status = status;
        }

        public List<BasicBean> getBasic() {
            return basic;
        }

        public void setBasic(List<BasicBean> basic) {
            this.basic = basic;
        }

        public static class BasicBean {
            /**
             * cid : CN101010100
             * location : 北京
             * parent_city : 北京
             * admin_area : 北京
             * cnty : 中国
             * lat : 39.90498734
             * lon : 116.4052887
             * tz : +8.00
             * type : city
             */

            private String cid;
            private String location;
            private String parent_city;
            private String admin_area;
            private String cnty;
            private String lat;
            private String lon;
            private String tz;
            private String type;

            public String getCid() {
                return cid;
            }

            public void setCid(String cid) {
                this.cid = cid;
            }

            public String getLocation() {
                return location;
            }

            public void setLocation(String location) {
                this.location = location;
            }

            public String getParent_city() {
                return parent_city;
            }

            public void setParent_city(String parent_city) {
                this.parent_city = parent_city;
            }

            public String getAdmin_area() {
                return admin_area;
            }

            public void setAdmin_area(String admin_area) {
                this.admin_area = admin_area;
            }

            public String getCnty() {
                return cnty;
            }

            public void setCnty(String cnty) {
                this.cnty = cnty;
            }

            public String getLat() {
                return lat;
            }

            public void setLat(String lat) {
                this.lat = lat;
            }

            public String getLon() {
                return lon;
            }

            public void setLon(String lon) {
                this.lon = lon;
            }

            public String getTz() {
                return tz;
            }

            public void setTz(String tz) {
                this.tz = tz;
            }

            public String getType() {
                return type;
            }

            public void setType(String type) {
                this.type = type;
            }
        }
    }
}

然后是修改ApiService

	/**
     * 搜索城市
     */
    @GET("/find?key=3086e91d66c04ce588a7f538f917c7f4&group=cn&number=10")
    Call<SearchCityResponse> searchCity(@Query("location") String location);

记住要用自己的key

② 新增搜索订阅器

SearchCityContract代码如下:

package com.llw.goodweather.contract;

import android.content.Context;

import com.llw.goodweather.api.ApiService;
import com.llw.goodweather.bean.SearchCityResponse;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
import com.llw.mvplibrary.net.NetCallBack;
import com.llw.mvplibrary.net.ServiceGenerator;

import retrofit2.Call;
import retrofit2.Response;

public class SearchCityContract {

    public static class SearchCityPresenter extends BasePresenter<ISearchCityView> {

        /**
         * 搜索城市
         * @param context
         * @param location
         */
        public void searchCity(final Context context, String location) {
            ApiService service = ServiceGenerator.createService(ApiService.class, 2);//指明访问的地址
            service.searchCity(location).enqueue(new NetCallBack<SearchCityResponse>() {
                @Override
                public void onSuccess(Call<SearchCityResponse> call, Response<SearchCityResponse> response) {
                    if(getView() != null){
                        getView().getSearchCityResult(response);
                    }
                }

                @Override
                public void onFailed() {
                    if(getView() != null){
                        getView().getDataFailed();
                    }
                }
            });
        }

    }

    public interface ISearchCityView extends BaseView {
        //查询城市返回数据
        void getSearchCityResult(Response<SearchCityResponse> response);
        //错误返回
        void getDataFailed();
    }
}

③ 新增搜索结果列表适配器

适配器里面加载数据和布局文件,数据有了,那么就去创建布局 首先要创建一个item, item_search_city_list.xml 布局中用到的图标

布局效果

布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:background="@color/white"
        android:gravity="center_vertical"
        android:padding="@dimen/dp_12"
        android:text="深圳"
        android:drawablePadding="@dimen/dp_8"
        android:textSize="@dimen/sp_16"
        android:textColor="@color/black"
        android:id="@+id/tv_city_name"
        android:foreground="@drawable/bg_white"
        android:drawableRight="@mipmap/icon_open"
        android:drawableLeft="@mipmap/icon_item_city"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <View
        android:layout_marginLeft="@dimen/dp_12"
        android:layout_marginRight="@dimen/dp_12"
        android:background="#EEEEEE"
        android:layout_width="match_parent"
        android:layout_height="1dp"/>
</LinearLayout>

item的布局写完了,接下来创建适配器SearchCityAdapter.java

代码如下:

package com.llw.goodweather.adapter;

import androidx.annotation.Nullable;

import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.llw.goodweather.R;
import com.llw.goodweather.bean.SearchCityResponse;

import java.util.List;

/**
 * 搜索城市结果列表适配器
 */
public class SearchCityAdapter extends BaseQuickAdapter<SearchCityResponse.HeWeather6Bean.BasicBean, BaseViewHolder> {
    public SearchCityAdapter(int layoutResId, @Nullable List<SearchCityResponse.HeWeather6Bean.BasicBean> data) {
        super(layoutResId, data);
    }

    @Override
    protected void convert(BaseViewHolder helper, SearchCityResponse.HeWeather6Bean.BasicBean item) {
        helper.setText(R.id.tv_city_name, item.getLocation());
        helper.addOnClickListener(R.id.tv_city_name);//绑定点击事件

    }
}

适配器写完了,下面写搜索页面的布局已经做数据渲染显示出来。

④ 修改搜索页面布局以及数据加载

搜索页面也是有两个图标的

icon_search.png

icon_delete.png

还有两个样式文件 cursor_style.xml 修改输入框的光标颜色

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <size android:width="1dp" />
    <solid android:color="#2C2C2C" />
</shape>

shape_gray_bg_14.xml 修改顶部布局的背景样式

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="@dimen/dp_14"/>
    <solid android:color="#F2F2F2"/>
</shape>

都是比较简单的,下面终于可以写页面的布局了 布局预览图

布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<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:fitsSystemWindows="true"
    android:orientation="vertical"
    tools:context=".ui.SearchCityActivity">

    <!--头部-->
    <androidx.appcompat.widget.Toolbar xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/white"
        android:elevation="@dimen/dp_6"
        app:contentInsetLeft="0dp"
        app:contentInsetStart="0dp"
        app:contentInsetStartWithNavigation="0dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navigationIcon="@mipmap/icon_return"
        app:popupTheme="@style/AppTheme.PopupOverlay">

        <!--输入框布局-->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="@dimen/dp_30"
            android:layout_marginRight="@dimen/dp_12"
            android:layout_weight="1"
            android:background="@drawable/shape_gray_bg_14"
            android:gravity="center_vertical"
            android:paddingLeft="@dimen/dp_12"
            android:paddingRight="@dimen/dp_12">
            <!--搜索图标-->
            <ImageView
                android:layout_width="@dimen/dp_16"
                android:layout_height="@dimen/dp_16"
                android:src="@mipmap/icon_search" />
            <!--输入框-->
            <AutoCompleteTextView
                android:id="@+id/edit_query"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:background="@null"
                android:completionThreshold="1"
                android:dropDownHorizontalOffset="5dp"
                android:hint="输入城市关键字"
                android:imeOptions="actionSearch"
                android:paddingLeft="@dimen/dp_8"
                android:paddingRight="@dimen/dp_4"
                android:singleLine="true"
                android:textColor="@color/black"
                android:textCursorDrawable="@drawable/cursor_style"
                android:textSize="@dimen/sp_14" />
            <!--清除输入的内容-->
            <ImageView
                android:id="@+id/iv_clear_search"
                android:layout_width="@dimen/dp_16"
                android:layout_height="@dimen/dp_16"
                android:src="@mipmap/icon_delete"
                android:visibility="gone" />
        </LinearLayout>
    </androidx.appcompat.widget.Toolbar>
    <!--搜索结果展示列表-->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="none" />
</LinearLayout>

这个输入框我这里有必要讲解一些

**android:imeOptions=“actionSearch”**就是将软键盘的回车改为搜索,这样可以增加用户的体验

接下来是SearchCityActivity.java页面代码的编写

先绑定布局文件中的控件

	@BindView(R.id.edit_query)
    AutoCompleteTextView editQuery;
    @BindView(R.id.iv_clear_search)
    ImageView ivClearSearch;
    @BindView(R.id.toolbar)
    Toolbar toolbar;
    @BindView(R.id.rv)
    RecyclerView rv;

然后定义

	List<SearchCityResponse.HeWeather6Bean.BasicBean> mList = new ArrayList<>();//数据源
    SearchCityAdapter mAdapter;//适配器

然后会定义五个方法,当然最开始里面是没有方法和处理逻辑,里面的方法都需要自己写,我下面贴的方法里面都是已经写好的

initData

	@Override
    public void initData(Bundle savedInstanceState) {
        StatusBarUtil.setStatusBarColor(context,R.color.white);//白色状态栏
        StatusBarUtil.StatusBarLightMode(context);//黑色字体
        Back(toolbar);

        initResultList();//初始化列表
        initEdit();//初始化输入框
    }

getLayoutId

	@Override
    public int getLayoutId() {
        return R.layout.activity_search_city;
    }

createPresent

	@Override
    protected SearchCityContract.SearchCityPresenter createPresent() {
        return new SearchCityContract.SearchCityPresenter();
    }

getSearchCityResult

	/**
     * 搜索城市返回的结果数据
     * @param response
     */
    @Override
    public void getSearchCityResult(Response<SearchCityResponse> response) {
        dismissLoadingDialog();
        if (("ok").equals(response.body().getHeWeather6().get(0).getStatus())) {
            if (response.body().getHeWeather6().get(0).getBasic().size() > 0) {
                mList.clear();
                mList.addAll(response.body().getHeWeather6().get(0).getBasic());
                mAdapter.notifyDataSetChanged();
                runLayoutAnimation(rv);
            } else {
                ToastUtils.showShortToast(context, "很抱歉,未找到相应的城市");
            }

        } else {
            ToastUtils.showShortToast(context, CodeToStringUtils.WeatherCode(response.body().getHeWeather6().get(0).getStatus()));
        }
    }

getDataFailed

	/**
     * 网络请求异常返回提示
     */
    @Override
    public void getDataFailed() {
        dismissLoadingDialog();//关闭弹窗
        ToastUtils.showShortToast(context, "网络异常");//这里的context是框架中封装好的,等同于this
    }

初始化列表

	//初始化列表
    private void initResultList() {
        mAdapter = new SearchCityAdapter(R.layout.item_search_city_list, mList);
        rv.setLayoutManager(new LinearLayoutManager(context));
        rv.setAdapter(mAdapter);

        mAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
            @Override
            public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
                ToastUtils.showShortToast(context, "点击了第 " + position + " 个");
            }
        });
    }

初始化输入框

	//初始化输入框
    private void initEdit() {
        editQuery.addTextChangedListener(textWatcher);//添加输入监听
        //监听软件键盘搜索按钮
        editQuery.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                    String location = editQuery.getText().toString();
                    if (!TextUtils.isEmpty(location)) {
                        showLoadingDialog();
                        mPresent.searchCity(context, location);
                    } else {
                        ToastUtils.showShortToast(context, "请输入搜索关键词");
                    }
                }
                return false;
            }
        });
    }

textWatcher

//输入监听
private TextWatcher textWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            if (!s.toString().equals("")) {//输入后,显示清除按钮
                ivClearSearch.setVisibility(View.VISIBLE);
            } else {//隐藏按钮
                ivClearSearch.setVisibility(View.GONE);
            }
        }
    };

点击事件

	//点击事件
    @OnClick(R.id.iv_clear_search)
    public void onViewClicked() {//清除输入框的内容
        ivClearSearch.setVisibility(View.GONE);//清除内容隐藏清除按钮
        editQuery.setText("");
    }

你可以将原来的onCreate()方法给删掉了。然后将我粘贴出来的代码复制进去就OK了,轻松愉快。

对了,还要修改主页面,右上角点击加号,出现的弹窗布局

这个截图应该就一目了然了吧,既然布局改动了,代码自然也要改。 先找到showAddWindow这个方法,在里面增加

那么既然代码写完了,结果怎么样呢?运行看一下效果吧!

以后能用GIF演示的我尽量不用静态图,这样看起来更直观一些,不是吗?

⑤ 查询城市天气

很好,我现在搜索城市地区是已经完成了,但是怎么去查看这个搜索到的城市的天气呢?我的想法是点击下面的某一项的通知将数据传递给MainActivity,同时关闭搜索的这个Activity。这就涉及到两个活动之间的数据传递通讯了,这里不建议你采用startActivity(intent)来跳转MainActivity。这里我们使用EventBus进行页面间的通讯,至于为什么用这个呢?我不告诉你,如果你真想知道,就留言,我再做解释 。 首先是引入依赖库文件

	//EventBus
    api 'org.greenrobot:eventbus:3.1.1'

改动build.gradle记得Sync一下

其实这个EventBus和广播差不多,不过也有区别,它的用法是,在哪个页面接收消息,就在哪个页面绑定和解绑消息。 下面来运用一下,首先是在项目包下创建一个eventbus包,包下创建一个SearchCityEvent的消息类

代码如下:

package com.llw.goodweather.eventbus;

/**
 * 搜索城市消息事件
 */
public class SearchCityEvent {

    public final String mLocation;
    public final String mCity;

    public SearchCityEvent(String location,String city) {
        this.mLocation = location;
        this.mCity = city;
    }
}

然后就是使用了,发送消息方,我们在SearchCityActivity中的item点击的时候发送消息

	//发送消息
	EventBus.getDefault().post(new SearchCityEvent(mList.get(position).getLocation(),
                        mList.get(position).getParent_city()));

然后是接收消息方

OK,现在运行一下看看效果吧!

然后我们在这里面放入接口请求

再运行一次

很好,基本功能已经实现了,接下来就是关于这个历史搜索记录的实现了。然后再修改MainActivity中点击跳转到搜索城市页面的代码

SPUtils.putBoolean(Constant.FLAG_OTHER_RETURN, false, context);//缓存标识

⑥ 增加历史搜索记录

首先使我们点击输入框的时候出现上一次输入的文字,可以设置一个默认的值,比如深圳有两个方法,一个是初始化数据,另一个是保存输入到的数据, 首先创建item的布局 item_tv_history.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:gravity="center_vertical"
    android:paddingLeft="@dimen/dp_16"
    android:layout_width="match_parent"
    android:singleLine="true"
    android:ellipsize="marquee"
    android:background="@color/white"
    android:textSize="@dimen/sp_14"
    android:textColor="@color/black"
    android:layout_height="@dimen/dp_40">
</TextView>

也比较简单,就一个TextView

然后就会业务代码了,代码如下

 /**
     * 使 AutoCompleteTextView在一开始获得焦点时自动提示
     *
     * @param field                保存在sharedPreference中的字段名
     * @param autoCompleteTextView 要操作的AutoCompleteTextView
     */
    private void initAutoComplete(String field, AutoCompleteTextView autoCompleteTextView) {
        SharedPreferences sp = getSharedPreferences("sp_history", 0);
        String etHistory = sp.getString("history", "深圳");//获取缓存
        String[] histories = etHistory.split(",");//通过,号分割成String数组
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item_tv_history, histories);

        // 只保留最近的50条的记录
        if (histories.length > 50) {
            String[] newHistories = new String[50];
            System.arraycopy(histories, 0, newHistories, 0, 50);
            adapter = new ArrayAdapter<String>(this, R.layout.item_tv_history, newHistories);
        }
        //AutoCompleteTextView可以直接设置数据适配器,并且在获得焦点的时候弹出,
        //通常是在用户第一次进入页面的时候,点击输入框输入的时候出现,如果每次都出现
        //是会应用用户体验的,这里不推荐这么做
        autoCompleteTextView.setAdapter(adapter);
        autoCompleteTextView.setOnFocusChangeListener(new View.OnFocusChangeListener() {

            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                AutoCompleteTextView view = (AutoCompleteTextView) v;
                if (hasFocus) {//出现历史输入记录
                    view.showDropDown();
                }
            }
        });
    }


    /**
     * 把指定AutoCompleteTextView中内容保存到sharedPreference中指定的字符段
     * 每次输入完之后调用此方法保存输入的值到缓存里
     *
     * @param field                保存在sharedPreference中的字段名
     * @param autoCompleteTextView 要操作的AutoCompleteTextView
     */
    private void saveHistory(String field, AutoCompleteTextView autoCompleteTextView) {

        String text = autoCompleteTextView.getText().toString();//输入的值
        SharedPreferences sp = getSharedPreferences("sp_history", 0);
        String tvHistory = sp.getString(field, "深圳");

        if (!tvHistory.contains(text + ",")) {//如果历史缓存中不存在输入的值则

            StringBuilder sb = new StringBuilder(tvHistory);
            sb.insert(0, text + ",");
            sp.edit().putString("history", sb.toString()).commit();//写入缓存

        }
    }

然后就是使用这两个方法了。

在点击软件盘搜索的时候,进行输入值的保存,然后在initData里面调用初始化方法

那么现在运行一下

OK,下面就要实现另一个功能了,就是搜索记录的动态布局展示,这个地方跟淘宝的那个搜索有点相似,实现这个功能需要自定义一个控件,还有样式,会比较麻烦,请一步一步来看。 这个样式和自定义控件的代码我都会放在mvplibrary下,首先是样式

样式代码:

	<!--历史记录-->
    <declare-styleable name="TagFlowLayout">
        <!--最大选择数量-->
        <attr name="max_select" format="integer" />
        <!--最大可显示行数-->
        <attr name="limit_line_count" format="integer" />
        <!--是否设置多行隐藏-->
        <attr name="is_limit" format="boolean" />
        <attr name="tag_gravity">
            <enum name="left" value="-1" />
            <enum name="center" value="0" />
            <enum name="right" value="1" />
        </attr>
    </declare-styleable>

接下来是自定义控件,我在view包下又建了一个flowlayout包,这个用于防止自定义控件需要用到的代码,这个代码来源于网络,并不是我自己敲出来的,这里我说明一下,以免造成不必要的麻烦,你只管复制粘贴即可。

FlowLayout

package com.llw.mvplibrary.view.flowlayout;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.LayoutDirection;
import android.view.View;
import android.view.ViewGroup;

import androidx.core.text.TextUtilsCompat;

import com.llw.mvplibrary.R;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;

public class FlowLayout extends ViewGroup {
    private static final String TAG = "FlowLayout";
    private static final int LEFT = -1;
    private static final int CENTER = 0;
    private static final int RIGHT = 1;

    private int limitLineCount; //默认显示3行 断词条显示3行,长词条显示2行
    private boolean isLimit; //是否有行限制
    private boolean isOverFlow; //是否溢出2行

    private int mGravity;
    protected List<List<View>> mAllViews = new ArrayList<List<View>>();
    protected List<Integer> mLineHeight = new ArrayList<Integer>();
    protected List<Integer> mLineWidth = new ArrayList<Integer>();
    private List<View> lineViews = new ArrayList<>();

    public boolean isOverFlow() {
        return isOverFlow;
    }

    private void setOverFlow(boolean overFlow) {
        isOverFlow = overFlow;
    }

    public boolean isLimit() {
        return isLimit;
    }

    public void setLimit(boolean limit) {
        if (!limit) {
            setOverFlow(false);
        }
        isLimit = limit;
    }

    public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout);
        mGravity = ta.getInt(R.styleable.TagFlowLayout_tag_gravity, LEFT);
        limitLineCount = ta.getInt(R.styleable.TagFlowLayout_limit_line_count, 3);
        isLimit = ta.getBoolean(R.styleable.TagFlowLayout_is_limit, false);
        int layoutDirection = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault());
        if (layoutDirection == LayoutDirection.RTL) {
            if (mGravity == LEFT) {
                mGravity = RIGHT;
            } else {
                mGravity = LEFT;
            }
        }
        ta.recycle();
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context) {
        this(context, null);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // wrap_content
        int width = 0;
        int height = 0;

        int lineWidth = 0;
        int lineHeight = 0;

        //在每一次换行之后记录,是否超过了行数
        int lineCount = 0;//记录当前的行数

        int cCount = getChildCount();

        for (int i = 0; i < cCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) {
                if (i == cCount - 1) {
                    if (isLimit) {
                        if (lineCount == limitLineCount) {
                            setOverFlow(true);
                            break;
                        } else {
                            setOverFlow(false);
                        }
                    }

                    width = Math.max(lineWidth, width);
                    height += lineHeight;
                    lineCount++;
                }
                continue;
            }
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

            int childWidth = child.getMeasuredWidth() + lp.leftMargin
                    + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin
                    + lp.bottomMargin;

            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
                if (isLimit) {
                    if (lineCount == limitLineCount) {
                        setOverFlow(true);
                        break;
                    } else {
                        setOverFlow(false);
                    }
                }
                width = Math.max(width, lineWidth);
                lineWidth = childWidth;
                height += lineHeight;
                lineHeight = childHeight;
                lineCount++;
            } else {
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
            if (i == cCount - 1) {
                if (isLimit) {
                    if (lineCount == limitLineCount) {
                        setOverFlow(true);
                        break;
                    }  else {
                        setOverFlow(false);
                    }
                }
                width = Math.max(lineWidth, width);
                height += lineHeight;
                lineCount++;
            }
        }
        setMeasuredDimension(
                modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
                modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
        );
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mAllViews.clear();
        mLineHeight.clear();
        mLineWidth.clear();
        lineViews.clear();

        int width = getWidth();

        int lineWidth = 0;
        int lineHeight = 0;

        //如果超过规定的行数则不进行绘制
        int lineCount = 0;//记录当前的行数

        int cCount = getChildCount();

        for (int i = 0; i < cCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE) continue;
            MarginLayoutParams lp = (MarginLayoutParams) child
                    .getLayoutParams();

            int childWidth = child.getMeasuredWidth();
            int childHeight = child.getMeasuredHeight();

            if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - getPaddingLeft() - getPaddingRight()) {
                if (isLimit) {
                    if (lineCount == limitLineCount) {
                        break;
                    }
                }

                mLineHeight.add(lineHeight);
                mAllViews.add(lineViews);
                mLineWidth.add(lineWidth);

                lineWidth = 0;
                lineHeight = childHeight + lp.topMargin + lp.bottomMargin;
                lineViews = new ArrayList<View>();
                lineCount++;
            }
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
            lineHeight = Math.max(lineHeight, childHeight + lp.topMargin
                    + lp.bottomMargin);
            lineViews.add(child);

        }
        mLineHeight.add(lineHeight);
        mLineWidth.add(lineWidth);
        mAllViews.add(lineViews);


        int left = getPaddingLeft();
        int top = getPaddingTop();

        int lineNum = mAllViews.size();

        for (int i = 0; i < lineNum; i++) {
            lineViews = mAllViews.get(i);
            lineHeight = mLineHeight.get(i);

            // set gravity
            int currentLineWidth = this.mLineWidth.get(i);
            switch (this.mGravity) {
                case LEFT:
                    left = getPaddingLeft();
                    break;
                case CENTER:
                    left = (width - currentLineWidth) / 2 + getPaddingLeft();
                    break;
                case RIGHT:
                    //  适配了rtl,需要补偿一个padding值
                    left = width - (currentLineWidth + getPaddingLeft()) - getPaddingRight();
                    //  适配了rtl,需要把lineViews里面的数组倒序排
                    Collections.reverse(lineViews);
                    break;
            }

            for (int j = 0; j < lineViews.size(); j++) {
                View child = lineViews.get(j);
                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                MarginLayoutParams lp = (MarginLayoutParams) child
                        .getLayoutParams();

                int lc = left + lp.leftMargin;
                int tc = top + lp.topMargin;
                int rc = lc + child.getMeasuredWidth();
                int bc = tc + child.getMeasuredHeight();

                child.layout(lc, tc, rc, bc);

                left += child.getMeasuredWidth() + lp.leftMargin
                        + lp.rightMargin;
            }
            top += lineHeight;
        }

    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }
}

RecordsDao

package com.llw.mvplibrary.view.flowlayout;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 历史记录搜索操作类
 */
public class RecordsDao {
    private final String TABLE_NAME = "records";
    private SQLiteDatabase recordsDb;
    private RecordSQLiteOpenHelper recordHelper;
    private NotifyDataChanged mNotifyDataChanged;
    private String mUsername;

    public RecordsDao(Context context, String username) {
        recordHelper = new RecordSQLiteOpenHelper(context);
        mUsername = username;
    }

    public interface NotifyDataChanged {
        void notifyDataChanged();
    }

    /**
     * 设置数据变化监听
     */
    public void setNotifyDataChanged(NotifyDataChanged notifyDataChanged) {
        mNotifyDataChanged = notifyDataChanged;
    }

    /**
     * 移除数据变化监听
     */
    public void removeNotifyDataChanged() {
        if (mNotifyDataChanged != null) {
            mNotifyDataChanged = null;
        }
    }

    private synchronized SQLiteDatabase getWritableDatabase() {
        return recordHelper.getWritableDatabase();
    }

    private synchronized SQLiteDatabase getReadableDatabase() {
        return recordHelper.getReadableDatabase();
    }

    /**
     * 如果考虑操作频繁可以到最后不用数据库时关闭
     * <p>
     * 关闭数据库
     */
    public void closeDatabase() {
        if (recordsDb != null) {
            recordsDb.close();
        }
    }

    /**
     * 添加搜索记录
     *
     * @param record 记录
     */
    public void addRecords(String record) {
        //如果这条记录没有则添加,有则更新时间
        int recordId = getRecordId(record);
        try {
            recordsDb = getReadableDatabase();
            if (-1 == recordId) {
                ContentValues values = new ContentValues();
                values.put("username", mUsername);
                values.put("keyword", record);
                //添加搜索记录
                recordsDb.insert(TABLE_NAME, null, values);
            } else {
                Date d = new Date();
                @SuppressLint("SimpleDateFormat") SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                //更新搜索历史数据时间
                ContentValues values = new ContentValues();
                values.put("time", sdf.format(d));
                recordsDb.update(TABLE_NAME, values, "_id = ?", new String[]{Integer.toString(recordId)});
            }
            if (mNotifyDataChanged != null) {
                mNotifyDataChanged.notifyDataChanged();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 判断是否含有该搜索记录
     *
     * @param record 记录
     * @return true | false
     */
    public boolean isHasRecord(String record) {
        boolean isHasRecord = false;
        Cursor cursor = null;
        try {
            recordsDb = getReadableDatabase();
            cursor = recordsDb.query(TABLE_NAME, null, "username = ?", new String[]{mUsername}, null, null, null);
            while (cursor.moveToNext()) {
                if (record.equals(cursor.getString(cursor.getColumnIndexOrThrow("keyword")))) {
                    isHasRecord = true;
                }
            }
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                //关闭游标
                cursor.close();
            }
        }
        return isHasRecord;
    }

    /**
     * 判断是否含有该搜索记录
     *
     * @param record 记录
     * @return id
     */
    public int getRecordId(String record) {
        int isHasRecord = -1;
        Cursor cursor = null;
        try {
            recordsDb = getReadableDatabase();
            cursor = recordsDb.query(TABLE_NAME, null, "username = ?", new String[]{mUsername}, null, null, null);
            while (cursor.moveToNext()) {
                if (record.equals(cursor.getString(cursor.getColumnIndexOrThrow("keyword")))) {
                    isHasRecord = cursor.getInt(cursor.getColumnIndexOrThrow("_id"));
                }
            }
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                //关闭游标
                cursor.close();
            }
        }
        return isHasRecord;
    }

    /**
     * 获取当前用户全部搜索记录
     *
     * @return 记录集合
     */
    public List<String> getRecordsList() {
        List<String> recordsList = new ArrayList<>();
        Cursor cursor = null;
        try {
            recordsDb = getReadableDatabase();
            cursor = recordsDb.query(TABLE_NAME, null, "username = ?", new String[]{mUsername}, null, null, "time desc");
            while (cursor.moveToNext()) {
                String name = cursor.getString(cursor.getColumnIndexOrThrow("keyword"));
                recordsList.add(name);
            }
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                //关闭游标
                cursor.close();
            }
        }
        return recordsList;
    }

    /**
     * 获取指定数量搜索记录
     *
     * @return 记录集合
     */
    public List<String> getRecordsByNumber(int recordNumber) {
        List<String> recordsList = new ArrayList<>();
        if (recordNumber < 0) {
            throw new IllegalArgumentException();
        } else if (0 == recordNumber) {
            return recordsList;
        } else {
            Cursor cursor = null;
            try {
                recordsDb = getReadableDatabase();
                cursor = recordsDb.query(TABLE_NAME, null, "username = ?", new String[]{mUsername}, null, null, "time desc limit " + recordNumber);
                while (cursor.moveToNext()) {
                    String name = cursor.getString(cursor.getColumnIndexOrThrow("keyword"));
                    recordsList.add(name);
                }
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } finally {
                if (cursor != null) {
                    //关闭游标
                    cursor.close();
                }
            }
        }
        return recordsList;
    }

    /**
     * 模糊查询
     *
     * @param record 记录
     * @return 返回类似记录
     */
    public List<String> querySimlarRecord(String record) {
        List<String> similarRecords = new ArrayList<>();
        Cursor cursor = null;
        try {
            recordsDb = getReadableDatabase();
            cursor = recordsDb.query(TABLE_NAME, null, "username = ? and keyword like '%?%'", new String[]{mUsername, record}, null, null, "order by time desc");
            while (cursor.moveToNext()) {
                String name = cursor.getString(cursor.getColumnIndexOrThrow("keyword"));
                similarRecords.add(name);
            }
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                //关闭游标
                cursor.close();
            }
        }
        return similarRecords;
    }

    /**
     * 清除指定用户的搜索记录
     */
    public void deleteUsernameAllRecords() {
        try {
            recordsDb = getWritableDatabase();
            recordsDb.delete(TABLE_NAME, "username = ?", new String[]{mUsername});
            if (mNotifyDataChanged != null) {
                mNotifyDataChanged.notifyDataChanged();
            }
        } catch (SQLException e) {
            e.printStackTrace();
            Log.e(TABLE_NAME, "清除所有历史记录失败");
        } finally {
        }
    }

    /**
     * 清空数据库所有的历史记录
     */
    public void deleteAllRecords() {
        try {
            recordsDb = getWritableDatabase();
            recordsDb.execSQL("delete from " + TABLE_NAME);
            if (mNotifyDataChanged != null) {
                mNotifyDataChanged.notifyDataChanged();
            }
        } catch (SQLException e) {
            e.printStackTrace();
            Log.e(TABLE_NAME, "清除所有历史记录失败");
        } finally {
        }
    }

    /**
     * 通过id删除记录
     *
     * @param id 记录id
     * @return 返回删除id
     */
    public int deleteRecord(int id) {
        int d = -1;
        try {
            recordsDb = getWritableDatabase();
            d = recordsDb.delete(TABLE_NAME, "_id = ?", new String[]{Integer.toString(id)});
            if (mNotifyDataChanged != null) {
                mNotifyDataChanged.notifyDataChanged();
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TABLE_NAME, "删除_id:" + id + "历史记录失败");
        }
        return d;
    }

    /**
     * 通过记录删除记录
     *
     * @param record 记录
     */
    public int deleteRecord(String record) {
        int recordId = -1;
        try {
            recordsDb = getWritableDatabase();
            recordId = recordsDb.delete(TABLE_NAME, "username = ? and keyword = ?", new String[]{mUsername, record});
            if (mNotifyDataChanged != null) {
                mNotifyDataChanged.notifyDataChanged();
            }
        } catch (SQLException e) {
            e.printStackTrace();
            Log.e(TABLE_NAME, "清除所有历史记录失败");
        }
        return recordId;
    }
}

RecordSQLiteOpenHelper

package com.llw.mvplibrary.view.flowlayout;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
 * 数据库帮助类
 */
public class RecordSQLiteOpenHelper extends SQLiteOpenHelper {
    private final static String DB_NAME = "search_history.db";
    private final static int DB_VERSION = 1;

    public RecordSQLiteOpenHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        String sqlStr = "CREATE TABLE IF NOT EXISTS records (_id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, keyword TEXT, time NOT NULL DEFAULT (datetime('now','localtime')));";
        db.execSQL(sqlStr);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

TagAdapter

package com.llw.mvplibrary.view.flowlayout;

import android.util.Log;
import android.view.View;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 布局适配器
 */
public abstract class TagAdapter<T> {
    private List<T> mTagData;
    private OnDataChangedListener mOnDataChangedListener;
    @Deprecated
    private HashSet<Integer> mCheckedPosList = new HashSet<Integer>();

    public TagAdapter(List<T> datas) {
        mTagData = datas;
    }

    public void setData(List<T> datas) {
        mTagData = datas;
    }

    @Deprecated
    public TagAdapter(T[] datas) {
        mTagData = new ArrayList<T>(Arrays.asList(datas));
    }

    interface OnDataChangedListener {
        void onChanged();
    }

    void setOnDataChangedListener(OnDataChangedListener listener) {
        mOnDataChangedListener = listener;
    }

    @Deprecated
    public void setSelectedList(int... poses) {
        Set<Integer> set = new HashSet<>();
        for (int pos : poses) {
            set.add(pos);
        }
        setSelectedList(set);
    }

    @Deprecated
    public void setSelectedList(Set<Integer> set) {
        mCheckedPosList.clear();
        if (set != null) {
            mCheckedPosList.addAll(set);
        }
        notifyDataChanged();
    }

    @Deprecated
    HashSet<Integer> getPreCheckedList() {
        return mCheckedPosList;
    }


    public int getCount() {
        return mTagData == null ? 0 : mTagData.size();
    }

    public void notifyDataChanged() {
        if (mOnDataChangedListener != null)
            mOnDataChangedListener.onChanged();
    }

    public T getItem(int position) {
        return mTagData.get(position);
    }

    public abstract View getView(FlowLayout parent, int position, T t);


    public void onSelected(int position, View view) {
        Log.d("llw", "onSelected " + position);
    }

    public void unSelected(int position, View view) {
        Log.d("llw", "unSelected " + position);
    }

    public boolean setSelected(int position, T t) {
        return false;
    }
}

TagFlowLayout

package com.llw.mvplibrary.view.flowlayout;

import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import com.llw.mvplibrary.R;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * 自定义控件
 */
public class TagFlowLayout extends FlowLayout
        implements TagAdapter.OnDataChangedListener {

    private static final String TAG = "TagFlowLayout";
    private TagAdapter mTagAdapter;
    private int mSelectedMax = -1;//-1为不限制数量

    private Set<Integer> mSelectedView = new HashSet<Integer>();

    private OnSelectListener mOnSelectListener;
    private OnTagClickListener mOnTagClickListener;
    private OnLongClickListener mOnLongClickListener;

    public interface OnSelectListener {
        void onSelected(Set<Integer> selectPosSet);
    }

    public interface OnTagClickListener {
        void onTagClick(View view, int position, FlowLayout parent);
    }

    public interface OnLongClickListener {
        void onLongClick(View view, int position);
    }

    public TagFlowLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout);
        mSelectedMax = ta.getInt(R.styleable.TagFlowLayout_max_select, -1);
        ta.recycle();
    }

    public TagFlowLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TagFlowLayout(Context context) {
        this(context, null);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int cCount = getChildCount();
        for (int i = 0; i < cCount; i++) {
            TagView tagView = (TagView) getChildAt(i);
            if (tagView.getVisibility() == View.GONE) {
                continue;
            }
            if (tagView.getTagView().getVisibility() == View.GONE) {
                tagView.setVisibility(View.GONE);
            }
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }


    public void setOnSelectListener(OnSelectListener onSelectListener) {
        mOnSelectListener = onSelectListener;
    }


    public void setOnTagClickListener(OnTagClickListener onTagClickListener) {
        mOnTagClickListener = onTagClickListener;
    }

    public void setOnLongClickListener(OnLongClickListener onLongClickListener) {
        mOnLongClickListener = onLongClickListener;
    }

    public void setAdapter(TagAdapter adapter) {
        mTagAdapter = adapter;
        mTagAdapter.setOnDataChangedListener(this);
        mSelectedView.clear();
        changeAdapter();
    }

    @SuppressWarnings("ResourceType")
    private void changeAdapter() {
        removeAllViews();
        TagAdapter adapter = mTagAdapter;
        TagView tagViewContainer = null;
        HashSet preCheckedList = mTagAdapter.getPreCheckedList();
        for (int i = 0; i < adapter.getCount(); i++) {
            View tagView = adapter.getView(this, i, adapter.getItem(i));

            tagViewContainer = new TagView(getContext());
            tagView.setDuplicateParentStateEnabled(true);
            if (tagView.getLayoutParams() != null) {
                tagViewContainer.setLayoutParams(tagView.getLayoutParams());


            } else {
                ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT);
                lp.setMargins(dip2px(getContext(), 5),
                        dip2px(getContext(), 5),
                        dip2px(getContext(), 5),
                        dip2px(getContext(), 5));
                tagViewContainer.setLayoutParams(lp);
            }
            ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            tagView.setLayoutParams(lp);
            tagViewContainer.addView(tagView);
            addView(tagViewContainer);

            if (preCheckedList.contains(i)) {
                setChildChecked(i, tagViewContainer);
            }

            if (mTagAdapter.setSelected(i, adapter.getItem(i))) {
                setChildChecked(i, tagViewContainer);
            }
            tagView.setClickable(false);
            final TagView finalTagViewContainer = tagViewContainer;
            final int position = i;
            tagViewContainer.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    doSelect(finalTagViewContainer, position);
                    if (mOnTagClickListener != null) {
                        mOnTagClickListener.onTagClick(finalTagViewContainer, position,
                                TagFlowLayout.this);
                    }
                }
            });

            tagViewContainer.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if (mOnLongClickListener != null) {
                        mOnLongClickListener.onLongClick(finalTagViewContainer, position);
                        //消费事件,不让事件继续下去
                        return true;
                    }
                    return false;
                }
            });
        }
        mSelectedView.addAll(preCheckedList);
    }

    public void setMaxSelectCount(int count) {
        if (mSelectedView.size() > count) {
            Log.w(TAG, "you has already select more than " + count + " views , so it will be clear .");
            mSelectedView.clear();
        }
        mSelectedMax = count;
    }

    public Set<Integer> getSelectedList() {
        return new HashSet<Integer>(mSelectedView);
    }

    private void setChildChecked(int position, TagView view) {
        view.setChecked(true);
        mTagAdapter.onSelected(position, view.getTagView());
    }

    private void setChildUnChecked(int position, TagView view) {
        view.setChecked(false);
        mTagAdapter.unSelected(position, view.getTagView());
    }

    private void doSelect(TagView child, int position) {
        if (!child.isChecked()) {
            //处理max_select=1的情况
            if (mSelectedMax == 1 && mSelectedView.size() == 1) {
                Iterator<Integer> iterator = mSelectedView.iterator();
                Integer preIndex = iterator.next();
                TagView pre = (TagView) getChildAt(preIndex);
                setChildUnChecked(preIndex, pre);
                setChildChecked(position, child);

                mSelectedView.remove(preIndex);
                mSelectedView.add(position);
            } else {
                if (mSelectedMax > 0 && mSelectedView.size() >= mSelectedMax) {
                    return;
                }
                setChildChecked(position, child);
                mSelectedView.add(position);
            }
        } else {
            setChildUnChecked(position, child);
            mSelectedView.remove(position);
        }
        if (mOnSelectListener != null) {
            mOnSelectListener.onSelected(new HashSet<Integer>(mSelectedView));
        }
    }

    public TagAdapter getAdapter() {
        return mTagAdapter;
    }


    private static final String KEY_CHOOSE_POS = "key_choose_pos";
    private static final String KEY_DEFAULT = "key_default";


    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putParcelable(KEY_DEFAULT, super.onSaveInstanceState());

        String selectPos = "";
        if (mSelectedView.size() > 0) {
            for (int key : mSelectedView) {
                selectPos += key + "|";
            }
            selectPos = selectPos.substring(0, selectPos.length() - 1);
        }
        bundle.putString(KEY_CHOOSE_POS, selectPos);
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;
            String mSelectPos = bundle.getString(KEY_CHOOSE_POS);
            if (!TextUtils.isEmpty(mSelectPos)) {
                String[] split = mSelectPos.split("\|");
                for (String pos : split) {
                    int index = Integer.parseInt(pos);
                    mSelectedView.add(index);

                    TagView tagView = (TagView) getChildAt(index);
                    if (tagView != null) {
                        setChildChecked(index, tagView);
                    }
                }
            }
            super.onRestoreInstanceState(bundle.getParcelable(KEY_DEFAULT));
            return;
        }
        super.onRestoreInstanceState(state);
    }


    @Override
    public void onChanged() {
        mSelectedView.clear();
        changeAdapter();
    }

    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

TagView

package com.llw.mvplibrary.view.flowlayout;

import android.content.Context;
import android.view.View;
import android.widget.Checkable;
import android.widget.FrameLayout;

/**
 * 自定义控件
 */
public class TagView extends FrameLayout implements Checkable {
    private boolean isChecked;
    private static final int[] CHECK_STATE = new int[]{android.R.attr.state_checked};

    public TagView(Context context) {
        super(context);
    }

    public View getTagView() {
        return getChildAt(0);
    }

    @Override
    public int[] onCreateDrawableState(int extraSpace) {
        int[] states = super.onCreateDrawableState(extraSpace + 1);
        if (isChecked()) {
            mergeDrawableStates(states, CHECK_STATE);
        }
        return states;
    }


    /**
     * @param checked The new checked state
     */
    @Override
    public void setChecked(boolean checked) {
        if (this.isChecked != checked) {
            this.isChecked = checked;
            refreshDrawableState();
        }
    }

    /**
     * @return The current checked state of the view
     */
    @Override
    public boolean isChecked() {
        return isChecked;
    }

    /**
     * Change the checked state of the view to the inverse of its current state
     */
    @Override
    public void toggle() {
        setChecked(!isChecked);
    }


}

现在创建布局和样式 shape_gray_bg_16.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="16dp"/>
    <solid android:color="#F8F8F8" />
</shape>

tv_history.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:gravity="center"
    android:layout_height="@dimen/dp_30"
    android:paddingLeft="@dimen/dp_12"
    android:paddingRight="@dimen/dp_12"
    android:paddingTop="@dimen/dp_4"
    android:paddingBottom="@dimen/dp_4"
    android:layout_margin="5dp"
    android:background="@drawable/shape_gray_bg_16"
    android:singleLine="true"
    android:text="搜索历史"
    android:textColor="@color/black"
    android:textSize="14sp"/>

然后在activity_search_city.xml中增加历史记录布局的代码

布局中用到了两个图标,分别是

icon_bottom.png

icon_delete_history.png

然后是历史搜索的布局代码

<!--历史搜索-->
    <LinearLayout
        android:id="@+id/ll_history_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/dp_4"
        android:layout_marginBottom="@dimen/dp_8"
        android:background="@color/white"
        android:orientation="vertical"
        android:paddingLeft="@dimen/dp_16"
        android:paddingTop="@dimen/dp_8"
        android:paddingRight="@dimen/dp_16">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <TextView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="搜索历史"
                android:textColor="@color/black"
                android:textSize="@dimen/sp_16" />

            <ImageView
                android:id="@+id/clear_all_records"
                android:layout_width="@dimen/dp_24"
                android:layout_height="@dimen/dp_24"
                android:background="@mipmap/icon_delete_history" />
        </LinearLayout>

        <com.llw.mvplibrary.view.flowlayout.TagFlowLayout
            android:id="@+id/fl_search_records"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="@dimen/dp_8"
            app:is_limit="true"
            app:limit_line_count="3"
            app:max_select="1" />

        <ImageView
            android:id="@+id/iv_arrow"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@mipmap/icon_bottom"
            android:visibility="gone" />
    </LinearLayout>

布局有了,下面就是写代码了, 然后就是在SearchCity中使用了 首先绑定视图

	@BindView(R.id.clear_all_records)
    ImageView clearAllRecords;//清理所有历史记录
    @BindView(R.id.fl_search_records)
    TagFlowLayout flSearchRecords;//搜索历史布局
    @BindView(R.id.iv_arrow)
    ImageView ivArrow;//超过三行就会出现,展开显示更多
    @BindView(R.id.ll_history_content)
    LinearLayout llHistoryContent;//搜索历史主布局

然后编写代码

我把之前初始化列表数据的代码也放到这个initView里面了,下面我贴一下代码

private void initView() {
        //默认账号
        String username = "007";
        //初始化数据库
        mRecordsDao = new RecordsDao(this, username);

        initTagFlowLayout();

        //创建历史标签适配器
        //为标签设置对应的内容
        mRecordsAdapter = new TagAdapter<String>(recordList) {

            @Override
            public View getView(FlowLayout parent, int position, String s) {
                TextView tv = (TextView) LayoutInflater.from(context).inflate(R.layout.tv_history,
                        flSearchRecords, false);
                //为标签设置对应的内容
                tv.setText(s);
                return tv;
            }
        };
        editQuery.addTextChangedListener(textWatcher);//添加输入监听
        editQuery.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                    String location = editQuery.getText().toString();
                    if (!TextUtils.isEmpty(location)) {
                        showLoadingDialog();
                        //添加数据
                        mRecordsDao.addRecords(location);
                        mPresent.searchCity(context, location);
                        //数据保存
                        saveHistory("history", editQuery);
                    } else {
                        ToastUtils.showShortToast(context, "请输入搜索关键词");
                    }
                }
                return false;
            }
        });

        flSearchRecords.setAdapter(mRecordsAdapter);
        flSearchRecords.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() {
            @Override
            public void onTagClick(View view, int position, FlowLayout parent) {
                //清空editText之前的数据
                editQuery.setText("");
                //将获取到的字符串传到搜索结果界面,点击后搜索对应条目内容
                editQuery.setText(recordList.get(position));
                editQuery.setSelection(editQuery.length());
            }
        });
        //长按删除某个条目
        flSearchRecords.setOnLongClickListener(new TagFlowLayout.OnLongClickListener() {
            @Override
            public void onLongClick(View view, final int position) {
                showDialog("确定要删除该条历史记录?", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        //删除某一条记录
                        mRecordsDao.deleteRecord(recordList.get(position));

                        initTagFlowLayout();
                    }
                });
            }
        });

        //view加载完成时回调
        flSearchRecords.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                boolean isOverFlow = flSearchRecords.isOverFlow();
                boolean isLimit = flSearchRecords.isLimit();
                if (isLimit && isOverFlow) {
                    ivArrow.setVisibility(View.VISIBLE);
                } else {
                    ivArrow.setVisibility(View.GONE);
                }
            }
        });

        //初始化搜索返回的数据列表
        mAdapter = new SearchCityAdapter(R.layout.item_search_city_list, mList);
        rv.setLayoutManager(new LinearLayoutManager(context));
        rv.setAdapter(mAdapter);

        mAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
            @Override
            public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
                SPUtils.putString(Constant.LOCATION, mList.get(position).getLocation(), context);
                //发送消息
                EventBus.getDefault().post(new SearchCityEvent(mList.get(position).getLocation(),
                        mList.get(position).getParent_city()));

                finish();
            }
        });

    }

    //历史记录布局
    private void initTagFlowLayout() {
        Observable.create(new ObservableOnSubscribe<List<String>>() {
            @Override
            public void subscribe(ObservableEmitter<List<String>> emitter) throws Exception {
                emitter.onNext(mRecordsDao.getRecordsByNumber(DEFAULT_RECORD_NUMBER));
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<List<String>>() {
                    @Override
                    public void accept(List<String> s) throws Exception {
                        recordList.clear();
                        recordList = s;
                        if (null == recordList || recordList.size() == 0) {
                            llHistoryContent.setVisibility(View.GONE);
                        } else {
                            llHistoryContent.setVisibility(View.VISIBLE);
                        }
                        if (mRecordsAdapter != null) {
                            mRecordsAdapter.setData(recordList);
                            mRecordsAdapter.notifyDataChanged();
                        }
                    }
                });
    }

这里面还有一个提示弹窗

	//提示弹窗  后续我可能会改,因为原生的太丑了
    private void showDialog(String dialogTitle, @NonNull DialogInterface.OnClickListener onClickListener) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setMessage(dialogTitle);
        builder.setPositiveButton("确定", onClickListener);
        builder.setNegativeButton("取消", null);
        builder.create().show();
    }

当然还有点击事件也要修改

	//点击事件
    @OnClick({R.id.iv_clear_search,R.id.clear_all_records, R.id.iv_arrow})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.iv_clear_search://清空输入的内容
                ivClearSearch.setVisibility(View.GONE);
                editQuery.setText("");
                break;
            case R.id.clear_all_records://清除所有记录
                showDialog("确定要删除全部历史记录?", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        flSearchRecords.setLimit(true);
                        //清除所有数据
                        mRecordsDao.deleteUsernameAllRecords();
                        llHistoryContent.setVisibility(View.GONE);
                    }
                });
                break;
            case R.id.iv_arrow://向下展开
                flSearchRecords.setLimit(false);
                mRecordsAdapter.notifyDataChanged();
                break;
        }
    }

这个点击事件的代码你可以把原来的点击事件直接覆盖掉,

为了不造成误会,我再粘贴一下SearchCityActivity的代码

package com.llw.goodweather.ui;

import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import com.chad.library.adapter.base.BaseQuickAdapter;
import com.llw.goodweather.R;
import com.llw.goodweather.adapter.SearchCityAdapter;
import com.llw.goodweather.bean.SearchCityResponse;
import com.llw.goodweather.contract.SearchCityContract;
import com.llw.goodweather.eventbus.SearchCityEvent;
import com.llw.goodweather.utils.CodeToStringUtils;
import com.llw.goodweather.utils.Constant;
import com.llw.goodweather.utils.SPUtils;
import com.llw.goodweather.utils.StatusBarUtil;
import com.llw.goodweather.utils.ToastUtils;
import com.llw.mvplibrary.mvp.MvpActivity;
import com.llw.mvplibrary.view.flowlayout.FlowLayout;
import com.llw.mvplibrary.view.flowlayout.RecordsDao;
import com.llw.mvplibrary.view.flowlayout.TagAdapter;
import com.llw.mvplibrary.view.flowlayout.TagFlowLayout;

import org.greenrobot.eventbus.EventBus;

import java.util.ArrayList;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.ObservableOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import retrofit2.Response;

import static com.llw.mvplibrary.utils.RecyclerViewAnimation.runLayoutAnimation;

/**
 * 搜索城市
 */
public class SearchCityActivity extends MvpActivity<SearchCityContract.SearchCityPresenter>
        implements SearchCityContract.ISearchCityView {


    @BindView(R.id.edit_query)
    AutoCompleteTextView editQuery;//输入框
    @BindView(R.id.iv_clear_search)
    ImageView ivClearSearch;//清空输入的内容图标
    @BindView(R.id.toolbar)
    Toolbar toolbar;
    @BindView(R.id.rv)
    RecyclerView rv;//数据显示列表
    @BindView(R.id.clear_all_records)
    ImageView clearAllRecords;//清理所有历史记录
    @BindView(R.id.fl_search_records)
    TagFlowLayout flSearchRecords;//搜索历史布局
    @BindView(R.id.iv_arrow)
    ImageView ivArrow;//超过三行就会出现,展开显示更多
    @BindView(R.id.ll_history_content)
    LinearLayout llHistoryContent;//搜索历史主布局



    List<SearchCityResponse.HeWeather6Bean.BasicBean> mList = new ArrayList<>();//数据源
    SearchCityAdapter mAdapter;//适配器

    private RecordsDao mRecordsDao;
    //默然展示词条个数
    private final int DEFAULT_RECORD_NUMBER = 10;
    private List<String> recordList = new ArrayList<>();
    private TagAdapter mRecordsAdapter;
    private LinearLayout mHistoryContent;

    @Override
    public void initData(Bundle savedInstanceState) {
        StatusBarUtil.setStatusBarColor(context, R.color.white);//白色状态栏
        StatusBarUtil.StatusBarLightMode(context);//黑色字体
        Back(toolbar);

        initView();//初始化页面数据
        initAutoComplete("history", editQuery);
    }

    private void initView() {
        //默认账号
        String username = "007";
        //初始化数据库
        mRecordsDao = new RecordsDao(this, username);

        initTagFlowLayout();

        //创建历史标签适配器
        //为标签设置对应的内容
        mRecordsAdapter = new TagAdapter<String>(recordList) {

            @Override
            public View getView(FlowLayout parent, int position, String s) {
                TextView tv = (TextView) LayoutInflater.from(context).inflate(R.layout.tv_history,
                        flSearchRecords, false);
                //为标签设置对应的内容
                tv.setText(s);
                return tv;
            }
        };
        editQuery.addTextChangedListener(textWatcher);//添加输入监听
        editQuery.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                    String location = editQuery.getText().toString();
                    if (!TextUtils.isEmpty(location)) {
                        showLoadingDialog();
                        //添加数据
                        mRecordsDao.addRecords(location);
                        mPresent.searchCity(context, location);
                        //数据保存
                        saveHistory("history", editQuery);
                    } else {
                        ToastUtils.showShortToast(context, "请输入搜索关键词");
                    }
                }
                return false;
            }
        });

        flSearchRecords.setAdapter(mRecordsAdapter);
        flSearchRecords.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() {
            @Override
            public void onTagClick(View view, int position, FlowLayout parent) {
                //清空editText之前的数据
                editQuery.setText("");
                //将获取到的字符串传到搜索结果界面,点击后搜索对应条目内容
                editQuery.setText(recordList.get(position));
                editQuery.setSelection(editQuery.length());
            }
        });
        //长按删除某个条目
        flSearchRecords.setOnLongClickListener(new TagFlowLayout.OnLongClickListener() {
            @Override
            public void onLongClick(View view, final int position) {
                showDialog("确定要删除该条历史记录?", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        //删除某一条记录
                        mRecordsDao.deleteRecord(recordList.get(position));

                        initTagFlowLayout();
                    }
                });
            }
        });

        //view加载完成时回调
        flSearchRecords.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                boolean isOverFlow = flSearchRecords.isOverFlow();
                boolean isLimit = flSearchRecords.isLimit();
                if (isLimit && isOverFlow) {
                    ivArrow.setVisibility(View.VISIBLE);
                } else {
                    ivArrow.setVisibility(View.GONE);
                }
            }
        });

        //初始化搜索返回的数据列表
        mAdapter = new SearchCityAdapter(R.layout.item_search_city_list, mList);
        rv.setLayoutManager(new LinearLayoutManager(context));
        rv.setAdapter(mAdapter);

        mAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
            @Override
            public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
                SPUtils.putString(Constant.LOCATION, mList.get(position).getLocation(), context);
                //发送消息
                EventBus.getDefault().post(new SearchCityEvent(mList.get(position).getLocation(),
                        mList.get(position).getParent_city()));

                finish();
            }
        });

    }

    //历史记录布局
    private void initTagFlowLayout() {
        Observable.create(new ObservableOnSubscribe<List<String>>() {
            @Override
            public void subscribe(ObservableEmitter<List<String>> emitter) throws Exception {
                emitter.onNext(mRecordsDao.getRecordsByNumber(DEFAULT_RECORD_NUMBER));
            }
        }).subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<List<String>>() {
                    @Override
                    public void accept(List<String> s) throws Exception {
                        recordList.clear();
                        recordList = s;
                        if (null == recordList || recordList.size() == 0) {
                            llHistoryContent.setVisibility(View.GONE);
                        } else {
                            llHistoryContent.setVisibility(View.VISIBLE);
                        }
                        if (mRecordsAdapter != null) {
                            mRecordsAdapter.setData(recordList);
                            mRecordsAdapter.notifyDataChanged();
                        }
                    }
                });
    }


    /**
     * 使 AutoCompleteTextView在一开始获得焦点时自动提示
     *
     * @param field                保存在sharedPreference中的字段名
     * @param autoCompleteTextView 要操作的AutoCompleteTextView
     */
    private void initAutoComplete(String field, AutoCompleteTextView autoCompleteTextView) {
        SharedPreferences sp = getSharedPreferences("sp_history", 0);
        String etHistory = sp.getString("history", "深圳");//获取缓存
        String[] histories = etHistory.split(",");//通过,号分割成String数组
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, R.layout.item_tv_history, histories);

        // 只保留最近的50条的记录
        if (histories.length > 50) {
            String[] newHistories = new String[50];
            System.arraycopy(histories, 0, newHistories, 0, 50);
            adapter = new ArrayAdapter<String>(this, R.layout.item_tv_history, newHistories);
        }
        //AutoCompleteTextView可以直接设置数据适配器,并且在获得焦点的时候弹出,
        //通常是在用户第一次进入页面的时候,点击输入框输入的时候出现,如果每次都出现
        //是会应用用户体验的,这里不推荐这么做
        autoCompleteTextView.setAdapter(adapter);
        autoCompleteTextView.setOnFocusChangeListener(new View.OnFocusChangeListener() {

            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                AutoCompleteTextView view = (AutoCompleteTextView) v;
                if (hasFocus) {//出现历史输入记录
                    view.showDropDown();
                }
            }
        });
    }


    /**
     * 把指定AutoCompleteTextView中内容保存到sharedPreference中指定的字符段
     * 每次输入完之后调用此方法保存输入的值到缓存里
     *
     * @param field                保存在sharedPreference中的字段名
     * @param autoCompleteTextView 要操作的AutoCompleteTextView
     */
    private void saveHistory(String field, AutoCompleteTextView autoCompleteTextView) {

        String text = autoCompleteTextView.getText().toString();//输入的值
        SharedPreferences sp = getSharedPreferences("sp_history", 0);
        String tvHistory = sp.getString(field, "深圳");

        if (!tvHistory.contains(text + ",")) {//如果历史缓存中不存在输入的值则

            StringBuilder sb = new StringBuilder(tvHistory);
            sb.insert(0, text + ",");
            sp.edit().putString("history", sb.toString()).commit();//写入缓存

        }
    }

    //提示弹窗  后续我可能会改,因为原生的太丑了
    private void showDialog(String dialogTitle, @NonNull DialogInterface.OnClickListener onClickListener) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setMessage(dialogTitle);
        builder.setPositiveButton("确定", onClickListener);
        builder.setNegativeButton("取消", null);
        builder.create().show();
    }

    @Override
    public int getLayoutId() {
        return R.layout.activity_search_city;
    }

    @Override
    protected SearchCityContract.SearchCityPresenter createPresent() {
        return new SearchCityContract.SearchCityPresenter();
    }

    //输入监听
    private TextWatcher textWatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {

        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {

        }

        @Override
        public void afterTextChanged(Editable s) {
            if (!s.toString().equals("")) {//输入后,显示清除按钮
                ivClearSearch.setVisibility(View.VISIBLE);
            } else {//隐藏按钮
                ivClearSearch.setVisibility(View.GONE);
            }
        }
    };

    //点击事件
    @OnClick({R.id.iv_clear_search,R.id.clear_all_records, R.id.iv_arrow})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.iv_clear_search://清空输入的内容
                ivClearSearch.setVisibility(View.GONE);
                editQuery.setText("");
                break;
            case R.id.clear_all_records://清除所有记录
                showDialog("确定要删除全部历史记录?", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        flSearchRecords.setLimit(true);
                        //清除所有数据
                        mRecordsDao.deleteUsernameAllRecords();
                        llHistoryContent.setVisibility(View.GONE);
                    }
                });
                break;
            case R.id.iv_arrow://向下展开
                flSearchRecords.setLimit(false);
                mRecordsAdapter.notifyDataChanged();
                break;
        }
    }

    /**
     * 搜索城市返回的结果数据
     *
     * @param response
     */
    @Override
    public void getSearchCityResult(Response<SearchCityResponse> response) {
        dismissLoadingDialog();
        if (("ok").equals(response.body().getHeWeather6().get(0).getStatus())) {
            if (response.body().getHeWeather6().get(0).getBasic().size() > 0) {
                mList.clear();
                mList.addAll(response.body().getHeWeather6().get(0).getBasic());
                mAdapter.notifyDataSetChanged();
                runLayoutAnimation(rv);

            } else {
                ToastUtils.showShortToast(context, "很抱歉,未找到相应的城市");
            }

        } else {
            ToastUtils.showShortToast(context, CodeToStringUtils.WeatherCode(response.body().getHeWeather6().get(0).getStatus()));
        }
    }

    /**
     * 网络请求异常返回提示
     */
    @Override
    public void getDataFailed() {
        dismissLoadingDialog();//关闭弹窗
        ToastUtils.showShortToast(context, "网络异常");//这里的context是框架中封装好的,等同于this
    }

}

最终效果图