最近公司项目中一直在搞地图开发,今天产品经理就给我布置了一些(无法想象)任务,其中一个就是实现地点搜索输入框的自动输入提示功能。拿到任务肯定想讨价还价一番,但是想到以前也写过,就不再负隅顽抗了。
以前在学校的时候实现过类似功能,是使用高德自带的InputtipsListener来实现的,想了解可以看看:文章传送点,这里就不详细介绍了。作为一名头脑发热的开发者,肯定不能安于现状,这里主要介绍其他两种方式 - poi实现和http请求接口实现,不管能不能成功,试了再说,撸起袖子就是干。先看看最终的效果:
做之前先分析一下功能需求,首先输入框中要添加内容清除的icon,当输入框有文字时,需要显示,为空时隐藏;接着,需要实现地址搜索功能并通过listview展示结果;最后需要实现展示搜索历史的功能。好的,那么下面我们来一步步实现。
其实,实现效果中的输入框并不难,只需要三个东西就够了:LinearLayout,EditText,ImageView。直接上代码吧,上了代码你就知道它到底有多简单了:
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginLeft="20dp"
android:background="@drawable/search_view_bg"
android:orientation="horizontal"
android:gravity="center_vertical">
<EditText
android:id="@+id/search_edit_text"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:hint="@string/input_cross_location"
android:textColorHint="#9B9B9B"
android:textSize="12sp"
android:maxLines="1"
android:layout_weight="1"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:background="@drawable/search_edit_bg"
android:drawableLeft="@mipmap/icon_edit_search"
android:drawablePadding="16dp"/>
<ImageView
android:id="@+id/search_edit_delete"
android:layout_width="12dp"
android:layout_height="12dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="8dp"
android:visibility="gone"
android:src="@mipmap/iocn_search_cancel"/>
</LinearLayout>
没错,这里为EditText父容器LinearLayout设置背景,然后EditText设置同样的背景,只不过需要将右边的圆角效果去掉,达到预期效果。也即是说,我们的输入框相当于是LinearLayout,里面包含了edittext和删除图标imageview,来看看drawable的代码吧:
search_view_bg:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false">
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--<solid android:color="#F4F4F4" />-->
<corners android:radius="3dp"/>
<solid android:color="#F3F3F3"/>
<!--<stroke android:color="#ffececec" android:width="1dp"/>-->
</shape>
</item>
<item android:state_window_focused="true">
<shape>
<corners android:radius="3dp"/>
<!--<stroke android:color="#ececec" android:width="1dp" />-->
<solid android:color="#F3F3F3"/>
<!--<solid android:color="#F4F4F4" />-->
</shape>
</item>
</selector>
search_edit_bg:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_window_focused="false">
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!--<solid android:color="#F4F4F4" />-->
<corners
android:topLeftRadius="3dp"
android:bottomLeftRadius="3dp"/>
<solid android:color="#F3F3F3"/>
<!--<stroke android:color="#ffececec" android:width="1dp"/>-->
</shape>
</item>
<item android:state_window_focused="true">
<shape>
<corners
android:topLeftRadius="3dp"
android:bottomLeftRadius="3dp"/>
<!--<stroke android:color="#ececec" android:width="1dp" />-->
<solid android:color="#F3F3F3"/>
<!--<solid android:color="#F4F4F4" />-->
</shape>
</item>
</selector>
ok,这就实现了最终的输入框UI,当然,你可以使用其他方式实现,比如自定义view,第三方开源等等,但我觉得这完全满足我们的需求,而且简单,不是吗?接下来,我们需要通过监听EditText的变化来实现搜索框中删除的变化,代码如下:
@Bind(R.id.search_edit_text)
EditText inputText;
@Bind(R.id.search_edit_delete)
ImageView buttonDelete;
......
buttonDelete.setOnClickListener(this);
inputText.addTextChangedListener(this);
......
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if(charSequence!=null){
buttonDelete.setVisibility(View.VISIBLE);
}else {
buttonDelete.setVisibility(View.GONE);
}
}
@Override
public void afterTextChanged(Editable editable) {
}
代码比较简单,就不解释了,不理解这个方法的可以谷歌一下,我们接着往下看。
用过高德地图api的开发者应该都知道里面有个常用的功能:POI搜索.高德提供了千万级别的 POI(Point of Interest,兴趣点)。在地图表达中,一个 POI 可代表一栋大厦、一家商铺、一处景点等等。通过POI搜索,完成找餐馆、找景点、找厕所等等的功能。如果我们的需求是获取周围兴趣点,那么搜索输入提示只要显示兴趣点就可以了。
下面我们来依次通过两种方法来实现快捷输入提示功能:
- POI搜索实现:
话不多说,直接上代码:
//方法一:使用poi搜索接口方法
private PoiResult poiResult; // poi返回的结果
private int currentPage = 0;// 当前页面,从0开始计数
private PoiSearch.Query query;// Poi查询条件类
private LatLonPoint latLonPoint;
private PoiSearch poiSearch;
private List<PoiItem> poiItems;// poi数据
private String keyWord;
private CommonAdapter adapter;
private final int ADDRESS_LOCATION_GET = 3242;
private String POI_SEARCH_TYPE = "汽车服务|汽车销售|" +
"//汽车维修|摩托车服务|餐饮服务|购物服务|生活服务|体育休闲服务|医疗保健服务|" +
"//住宿服务|风景名胜|商务住宅|政府机构及社会团体|科教文化服务|交通设施服务|" +
"//金融保险服务|公司企业|道路附属设施|地名地址信息|公共设施";
......
/**
* 开始进行poi搜索
*/
protected void doSearchQuery() {
latLonPoint = new LatLonPoint(MyApplication.mapLocation.getLatitude(), MyApplication.mapLocation.getLongitude());// 116.472995,39.993743
keyWord = inputText.getText().toString().trim();
currentPage = 0;
//keyWord表示搜索字符串,
//第二个参数表示POI搜索类型,二者选填其一,选用POI搜索类型时建议填写类型代码,码表可以参考下方(而非文字)
//cityCode表示POI搜索区域,可以是城市编码也可以是城市名称,也可以传空字符串,空字符串代表全国在全国范围内进行搜索
query = new PoiSearch.Query(keyWord, POI_SEARCH_TYPE, "");
query.setPageSize(30);// 设置每页最多返回多少条poiItem
query.setPageNum(currentPage);// 设置查第一页
if (latLonPoint != null) {
poiSearch = new PoiSearch(this, query);
poiSearch.setOnPoiSearchListener(this);
poiSearch.setBound(new PoiSearch.SearchBound(latLonPoint, 3000, true));//设置搜索范围
poiSearch.searchPOIAsyn();// 异步搜索
}
}
......
@Override
public void onPoiSearched(PoiResult result, int code) {
//DialogUtils.dismissProgressDialog();
if (code == AMapException.CODE_AMAP_SUCCESS) {
if (result != null && result.getQuery() != null) {// 搜索poi的结果
loge("搜索的code为===="+code+", result数量=="+result.getPois().size());
if (result.getQuery().equals(query)) {// 是否是同一次搜索
poiResult = result;
loge("搜索的code为===="+code+", result数量=="+poiResult.getPois().size());
List<SuggestionCity> suggestionCities = poiResult.getSearchSuggestionCitys();// 当搜索不到poiitem数据时,会返回含有搜索关键字的城市信息
if (poiItems != null && poiItems.size() > 0) {
poiItems.clear();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}
poiItems = poiResult.getPois();// 取得第一页的poiitem数据,页数从数字0开始
//通过listview显示搜索结果的操作省略
......
}
} else {
loge("没有搜索结果");
toast(getString(R.string.search_no_result));
empty_view.setText(getString(R.string.search_no_result));
}
} else {
loge("搜索出现错误");
toast(getString(R.string.search_error));
empty_view.setText(getString(R.string.search_error));
}
}
@Override
public void onPoiItemSearched(PoiItem poiItem, int i) {
}
注释都比较清楚,大家理解起来应该也不难,具体用法可以参考高德官方文档,可以直接在onTextChangeed()方法中判断是否有内容来调用doSearchQuery()方法即可。
- 通过实时访问http接口实现:
除了以上方法实现,还可以用高德提供的web端API接口实现功能,详情见高德web服务开发文档。我们可以直接通过请求高德为我们提供的搜索url接口来访问并获取数据,输入提示API服务地址为:
http://restapi.amap.com/v3/assistant/inputtips?
需要我们填充相应的字段,如key,keyword等,具体介绍看官方文档就可以了,大波代码来袭:
//方法二:使用http请求返回搜索结果
private List<POISearchResultBean.Tips> tipsList;
private POISearchResultBean resultBean;
private String locationString;
private String lon;
private String lat;
private final int SEARCH_OK = 3266;
@Bind(R.id.search_result_listview)
ListView resultListView;
.......
private MapSerchActivity.MyWeakReferenceHandler handler = new MapSerchActivity.MyWeakReferenceHandler(this) {
@Override
public void handleMessage(Message msg, Activity weakReferenceActivity) {
if (msg.what == ADDRESS_LOCATION_GET) {
if (tipsList != null && tipsList.size() > 0) {
if (adapter == null && resultListView != null) {
//wrong
resultListView.setAdapter(adapter = new CommonAdapter<POISearchResultBean.Tips>(SearchAddressActivity.this, tipsList, R.layout.search_result_item) {
@Override
public void convert(ViewHolder helper, final POISearchResultBean.Tips item) {
helper.setText(R.id.search_result_item_address_name, item.getName());
helper.setText(R.id.search_result_item_address_detail, item.getDistrict()+item.getAddress());
helper.getView(R.id.search_result_item_address_layout).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loge("点击了item");
toast(item.getName());
boolean hasData = hasData(item.getName());
if (!hasData) {
insertData(item.getName());
//queryData("");
}
locationString = item.getLocation();
lon = locationString.substring(0,locationString.indexOf(","));
lat = locationString.substring(locationString.indexOf(",")+1,locationString.length());
loge("经纬度信息为==="+lon+","+lat);
Intent intent = new Intent();
intent.putExtra("location_lon",lon);
intent.putExtra("location_lat",lat);
setResult(SEARCH_OK, intent);
finish();
}
});
}
});
} else {
adapter = null;
resultListView.setAdapter(adapter = new CommonAdapter<POISearchResultBean.Tips>(SearchAddressActivity.this, tipsList, R.layout.search_result_item) {
@Override
public void convert(ViewHolder helper, final POISearchResultBean.Tips item) {
helper.setText(R.id.search_result_item_address_name, item.getName());
helper.setText(R.id.search_result_item_address_detail, item.getDistrict()+item.getAddress());
helper.getView(R.id.search_result_item_address_layout).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// loge("点击了item");
loge("点击了item");
toast(item.getName());
boolean hasData = hasData(item.getName());
if (!hasData) {
insertData(item.getName());
//queryData("");
}
locationString = item.getLocation();
lon = locationString.substring(0,locationString.indexOf(","));
lat = locationString.substring(locationString.indexOf(",")+1,locationString.length());
loge("经纬度信息为==="+lon+"====="+lat);
Intent intent = new Intent();
intent.putExtra("location_lon",lon);
intent.putExtra("location_lat",lat);
setResult(SEARCH_OK, intent);
finish();
}
});
}
});
}
}
}
}
};
.......
/**
* by moos on 2017/09/11
* func:http请求返回关键词搜索结果
* 请求路径范例:http://restapi.amap.com/v3/assistant/inputtips?key=您的key&keywords=肯德基&types=050301&location=116.481488,39.990464&city=北京&datatype=all
*/
private void searchAddressByHttp(String keyWord){
//DialogUtils.createProgressDialog(SearchAddressActivity.this,"Searching...");
OkHttpUtils
.get()
.url(HttpAPI.AMAP_POI_SEARCH_URL + "key="+Const.amap_poi_search_key+"&keywords="+keyWord)
.build()
.execute(new StringCallback() {
@Override
public void onError(Call call, Exception e, int id) {
loge("获取http poi搜索结果失败=" + e.getMessage());
//DialogUtils.dismissProgressDialog();
Toast.makeText(SearchAddressActivity.this, getString(R.string.act_qr_code_fail), Toast.LENGTH_LONG).show();
}
@Override
public void onResponse(String response, int id) {
Logger.e("获取http poi搜索结果 =" + response);
resultBean = JSONObject.parseObject(response, POISearchResultBean.class);
if (resultBean.getStatus()==1) {
//处理和显示搜索数据列表
if (tipsList != null && tipsList.size() > 0) {
tipsList.clear();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}
tipsList = resultBean.getTips();
Message message = Message.obtain(handler);
message.what = ADDRESS_LOCATION_GET;
handler.sendMessage(message);
} else {
toast("搜索失败,请重新尝试");
}
}
});
}
通过okhttp请求网络接口有很多大神封装好的工具库,这里我使用的鸿神的okHttpUtils,大家可以根据自己的需要来选择。同时,这里使用了CommonAdapter来作为listview的适配器,同样是鸿神的杰作,如果你对它不熟悉,建议去看一下这篇文章:打造listview万能适配器。其他的就没什么难点了,关键还是靠自己研究和练习一下了。
最后,让我们来看看如何实现展示搜索历史的功能吧。先分析一下需求:首先进入到搜索界面要展示搜索历史列表,然后可以点击列表下方的清空历史来清除数据,接着,当我们搜索地名并选中时,自动存入搜索历史。其实,说到底,就是两个小功能,数据存储和数据展示,下面依次来探讨如何实现。
- 搜索历史数据的存储:
一般地,我们会将搜索的历史数据保存在本地。常用的两种方式分别为数据库存储和sp(SharedPreference)存储,两种方式都可以实现我们的需求,这里我才用的是数据库,有时间的话大家可以试试sp存储方式。这里不研究数据库的基本用法比较简单,就一笔带过了,直接上代码:
首先是创建数据库:
/**
* Created by moos on 17/9/11.
*/
public class SearchHistorySQLiteHelper extends SQLiteOpenHelper {
private static String name = "search.db";
private static Integer version = 1;
public SearchHistorySQLiteHelper(Context context) {
super(context, name, null, version);
}
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL("create table history(id integer primary key autoincrement,name varchar(200))");
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
}
}
具体操作:
//历史搜索功能
private SearchHistorySQLiteHelper helper = new SearchHistorySQLiteHelper(this);
private SQLiteDatabase db;
private BaseAdapter baseAdapter;
@Bind(R.id.search_history_listview)
ListView search_history_listView;
@Bind(R.id.search_history_view)
LinearLayout search_history_view;
......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search_address);
ButterKnife.bind(this);
initView();
}
private void initView(){
buttonCancel.setOnClickListener(this);
buttonDelete.setOnClickListener(this);
inputText.addTextChangedListener(this);
search_history_listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
loge("点击了第"+position+"个搜索历史item");
TextView textView = (TextView) view.findViewById(R.id.search_history_item_address_name);
String name = textView.getText().toString();
inputText.setText(name);
Toast.makeText(SearchAddressActivity.this, name, Toast.LENGTH_SHORT).show();
}
});
// 第一次进入查询所有的历史记录
queryData("");
}
......
//采用本地数据库存储
/**
* 插入数据
*/
private void insertData(String tempName) {
db = helper.getWritableDatabase();
db.execSQL("insert into history(name) values('" + tempName + "')");
db.close();
}
/**
* 模糊查询数据
*/
private void queryData(String tempName) {
Cursor cursor = helper.getReadableDatabase().rawQuery(
"select id as _id,name from history where name like '%" + tempName + "%' order by id desc ", null);
// 创建adapter适配器对象
baseAdapter = new SimpleCursorAdapter(this, R.layout.search_history_item, cursor, new String[] { "name" },
new int[] { R.id.search_history_item_address_name }, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
//添加footerView
View footerView = LayoutInflater.from(this).inflate(R.layout.delete_search_history_bt,null);
search_history_listView.addFooterView(footerView,null,false);
footerView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
deleteData();
toast("清除成功");
}
});
// 设置适配器
search_history_listView.setAdapter(baseAdapter);
baseAdapter.notifyDataSetChanged();
if(baseAdapter.getCount()==0){
//无历史搜索记录
search_history_view.setVisibility(View.GONE);
}
}
/**
* 检查数据库中是否已经有该条记录
*/
private boolean hasData(String tempName) {
Cursor cursor = helper.getReadableDatabase().rawQuery(
"select id as _id,name from history where name =?", new String[]{tempName});
//判断是否有下一个
return cursor.moveToNext();
}
/**
* 清空数据
*/
private void deleteData() {
db = helper.getWritableDatabase();
db.execSQL("delete from history");
db.close();
loge("搜索历史数据删除成功");
queryData("");
}
数据库的操作网上很多教程和文章,这里就不多加解释,主要说一下逻辑吧。首先,我们输入关键词搜索到相应的目标地点后,点击回调中,即插入一条该地名的数据。当然,为了防止重复,我们需要判断一下数据库中是否已经存在该数据。我们刚进入搜索界面的时候,需要查询数据库中所有的数据并展示。
- 搜索历史数据的展示和删除:
展示的话没什么太大的问题,一般采用listview展示并为item加上点击事件就OK了,主要是要设置好展示数据和刷新数据的逻辑。这里主要提一下如何实现下方"清除搜索历史"的友好展示以及逻辑。
如果有人问你如何快速实现listview下面的button依附效果,你会怎么回答?常见的回答一般有两种:
1.在listview下面放一个button,然后外面套一层ScrollerView
2.使用listview的addFooterView()给其添加底部布局
虽然两种方式很容易想到,但是对于我们这些新手来说,动手实现起来多少有些弯弯绕绕。比如第一种方式,我们需要考虑如何处理嵌套滑动的问题,至于第二种,无非是研究listview之footerview的用法,当然,抛开各自的难点,第二种方式无非更加优雅一些,所以,这里只讨论如何使用该方式实现。
首先看一下footerview的布局:
search_history_item:
<?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"
android:gravity="center_horizontal">
<TextView
android:id="@+id/item_search_history_delete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:gravity="center"
android:text="清楚搜索历史"
android:textSize="12sp"
android:layout_marginBottom="15dp"
android:textColor="#828282"/>
</LinearLayout>
listview添加footerview比较简单,只通过简单的两行代码:
//添加footerView
View footerView = LayoutInflater.from(this).inflate(R.layout.delete_search_history_bt,null);
search_history_listView.addFooterView(footerView,null,false);
便可以实现,但是footerview的点击事件如何获取呢?很多人说直接用onItenClickListener()呀,但是,大家可以通过log或者toast看看,点击footerview是否真的响应了。答案是 - 并没有。我们应该尽量避免在onITemClickListener回调方法中实现footerview点击事件,因为position并没有变化,上限依旧是原来的adapter.getCount()。我们可以先禁止footerview在item中的点击响应,即addFooterView()方法第三个参数设为false,然后给footerView单独设置点击事件:
footerView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
deleteData();
toast("清除成功");
}
});
到这里,我们就完整实现了地图的搜索功能了,虽然实现的方式比较简单,但还是学到一些东西的。后面有时间会将该部分功能做个demo单独分享出来。另外大家如果有什么问题和优化建议,欢迎留言反馈,不胜感激😆。
最后,请教一下大家mac如何录制gif呢,为什么我用licecap录制出来的是黑屏呢(⊙﹏⊙).