效果图如下:
前言
看这篇文章之前,你是否是一路看过来的呢?如果你单独看着一篇的话,有些内容你可能看不懂,所以我建议你一篇一篇的看,这是第十五篇文章了,前面还有十四篇,建议先了解一下,传送门:
天气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 } }
最终效果图