最近公司項目中一直在搞地圖開發(fā),今天產(chǎn)品經(jīng)理就給我布置了一些(無法想象)任務(wù),其中一個就是實現(xiàn)地點搜索輸入框的自動輸入提示功能视事。拿到任務(wù)肯定想討價還價一番,但是想到以前也寫過,就不再負(fù)隅頑抗了饲梭。
??以前在學(xué)校的時候?qū)崿F(xiàn)過類似功能,是使用高德自帶的InputtipsListener來實現(xiàn)的,想了解可以看看:文章傳送點,這里就不詳細(xì)介紹了。作為一名頭腦發(fā)熱的開發(fā)者,肯定不能安于現(xiàn)狀,這里主要介紹其他兩種方式 - poi實現(xiàn)和http請求接口實現(xiàn),不管能不能成功,試了再說,擼起袖子就是干捞魁。先看看最終的效果:
做之前先分析一下功能需求,首先輸入框中要添加內(nèi)容清除的icon,當(dāng)輸入框有文字時,需要顯示,為空時隱藏;接著,需要實現(xiàn)地址搜索功能并通過listview展示結(jié)果;最后需要實現(xiàn)展示搜索歷史的功能澎怒。好的,那么下面我們來一步步實現(xiàn)现诀。
其實,實現(xiàn)效果中的輸入框并不難,只需要三個東西就夠了: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設(shè)置背景,然后EditText設(shè)置同樣的背景,只不過需要將右邊的圓角效果去掉,達(dá)到預(yù)期效果鞍时。也即是說,我們的輸入框相當(dāng)于是LinearLayout,里面包含了edittext和刪除圖標(biāo)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,這就實現(xiàn)了最終的輸入框UI,當(dāng)然,你可以使用其他方式實現(xiàn),比如自定義view,第三方開源等等,但我覺得這完全滿足我們的需求,而且簡單,不是嗎?接下來,我們需要通過監(jiān)聽EditText的變化來實現(xiàn)搜索框中刪除的變化,代碼如下:
@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的開發(fā)者應(yīng)該都知道里面有個常用的功能:POI搜索.高德提供了千萬級別的 POI(Point of Interest哮翘,興趣點)。在地圖表達(dá)中,一個 POI 可代表一棟大廈尤勋、一家商鋪凑懂、一處景點等等盒齿。通過POI搜索成福,完成找餐館、找景點灵再、找?guī)鹊鹊墓δ芾卟恪H绻覀兊男枨笫谦@取周圍興趣點,那么搜索輸入提示只要顯示興趣點就可以了。
??下面我們來依次通過兩種方法來實現(xiàn)快捷輸入提示功能:
- POI搜索實現(xiàn):
話不多說,直接上代碼:
//方法一:使用poi搜索接口方法
private PoiResult poiResult; // poi返回的結(jié)果
private int currentPage = 0;// 當(dāng)前頁面翎迁,從0開始計數(shù)
private PoiSearch.Query query;// Poi查詢條件類
private LatLonPoint latLonPoint;
private PoiSearch poiSearch;
private List<PoiItem> poiItems;// poi數(shù)據(jù)
private String keyWord;
private CommonAdapter adapter;
private final int ADDRESS_LOCATION_GET = 3242;
private String POI_SEARCH_TYPE = "汽車服務(wù)|汽車銷售|" +
"http://汽車維修|摩托車服務(wù)|餐飲服務(wù)|購物服務(wù)|生活服務(wù)|體育休閑服務(wù)|醫(yī)療保健服務(wù)|" +
"http://住宿服務(wù)|風(fēng)景名勝|(zhì)商務(wù)住宅|政府機(jī)構(gòu)及社會團(tuán)體|科教文化服務(wù)|交通設(shè)施服務(wù)|" +
"http://金融保險服務(wù)|公司企業(yè)|道路附屬設(shè)施|地名地址信息|公共設(shè)施";
......
/**
* 開始進(jìn)行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表示搜索字符串栋猖,
//第二個參數(shù)表示POI搜索類型,二者選填其一汪榔,選用POI搜索類型時建議填寫類型代碼掂铐,碼表可以參考下方(而非文字)
//cityCode表示POI搜索區(qū)域,可以是城市編碼也可以是城市名稱揍异,也可以傳空字符串,空字符串代表全國在全國范圍內(nèi)進(jìn)行搜索
query = new PoiSearch.Query(keyWord, POI_SEARCH_TYPE, "");
query.setPageSize(30);// 設(shè)置每頁最多返回多少條poiItem
query.setPageNum(currentPage);// 設(shè)置查第一頁
if (latLonPoint != null) {
poiSearch = new PoiSearch(this, query);
poiSearch.setOnPoiSearchListener(this);
poiSearch.setBound(new PoiSearch.SearchBound(latLonPoint, 3000, true));//設(shè)置搜索范圍
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的結(jié)果
loge("搜索的code為===="+code+", result數(shù)量=="+result.getPois().size());
if (result.getQuery().equals(query)) {// 是否是同一次搜索
poiResult = result;
loge("搜索的code為===="+code+", result數(shù)量=="+poiResult.getPois().size());
List<SuggestionCity> suggestionCities = poiResult.getSearchSuggestionCitys();// 當(dāng)搜索不到poiitem數(shù)據(jù)時爆班,會返回含有搜索關(guān)鍵字的城市信息
if (poiItems != null && poiItems.size() > 0) {
poiItems.clear();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}
poiItems = poiResult.getPois();// 取得第一頁的poiitem數(shù)據(jù)衷掷,頁數(shù)從數(shù)字0開始
//通過listview顯示搜索結(jié)果的操作省略
......
}
} else {
loge("沒有搜索結(jié)果");
toast(getString(R.string.search_no_result));
empty_view.setText(getString(R.string.search_no_result));
}
} else {
loge("搜索出現(xiàn)錯誤");
toast(getString(R.string.search_error));
empty_view.setText(getString(R.string.search_error));
}
}
@Override
public void onPoiItemSearched(PoiItem poiItem, int i) {
}
注釋都比較清楚,大家理解起來應(yīng)該也不難,具體用法可以參考高德官方文檔,可以直接在onTextChangeed()方法中判斷是否有內(nèi)容來調(diào)用doSearchQuery()方法即可。
- 通過實時訪問http接口實現(xiàn):
除了以上方法實現(xiàn),還可以用高德提供的web端API接口實現(xiàn)功能,詳情見高德web服務(wù)開發(fā)文檔柿菩。我們可以直接通過請求高德為我們提供的搜索url接口來訪問并獲取數(shù)據(jù),輸入提示API服務(wù)地址為:
http://restapi.amap.com/v3/assistant/inputtips?
需要我們填充相應(yīng)的字段,如key,keyword等,具體介紹看官方文檔就可以了,大波代碼來襲:
//方法二:使用http請求返回搜索結(jié)果
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("經(jīng)緯度信息為==="+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("經(jīng)緯度信息為==="+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請求返回關(guān)鍵詞搜索結(jié)果
* 請求路徑范例: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搜索結(jié)果失敗=" + 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搜索結(jié)果 =" + response);
resultBean = JSONObject.parseObject(response, POISearchResultBean.class);
if (resultBean.getStatus()==1) {
//處理和顯示搜索數(shù)據(jù)列表
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請求網(wǎng)絡(luò)接口有很多大神封裝好的工具庫,這里我使用的鴻神的okHttpUtils,大家可以根據(jù)自己的需要來選擇戚嗅。同時,這里使用了CommonAdapter來作為listview的適配器,同樣是鴻神的杰作,如果你對它不熟悉,建議去看一下這篇文章:打造listview萬能適配器。其他的就沒什么難點了,關(guān)鍵還是靠自己研究和練習(xí)一下了枢舶。
最后,讓我們來看看如何實現(xiàn)展示搜索歷史的功能吧懦胞。先分析一下需求:首先進(jìn)入到搜索界面要展示搜索歷史列表,然后可以點擊列表下方的清空歷史來清除數(shù)據(jù),接著,當(dāng)我們搜索地名并選中時,自動存入搜索歷史。其實,說到底,就是兩個小功能,數(shù)據(jù)存儲和數(shù)據(jù)展示,下面依次來探討如何實現(xiàn)凉泄。
- 搜索歷史數(shù)據(jù)的存儲:
一般地,我們會將搜索的歷史數(shù)據(jù)保存在本地躏尉。常用的兩種方式分別為數(shù)據(jù)庫存儲和sp(SharedPreference)存儲,兩種方式都可以實現(xiàn)我們的需求,這里我才用的是數(shù)據(jù)庫,有時間的話大家可以試試sp存儲方式。這里不研究數(shù)據(jù)庫的基本用法比較簡單,就一筆帶過了,直接上代碼:
首先是創(chuàng)建數(shù)據(jù)庫:
/**
* 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();
}
});
// 第一次進(jìn)入查詢所有的歷史記錄
queryData("");
}
......
//采用本地數(shù)據(jù)庫存儲
/**
* 插入數(shù)據(jù)
*/
private void insertData(String tempName) {
db = helper.getWritableDatabase();
db.execSQL("insert into history(name) values('" + tempName + "')");
db.close();
}
/**
* 模糊查詢數(shù)據(jù)
*/
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);
// 創(chuàng)建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("清除成功");
}
});
// 設(shè)置適配器
search_history_listView.setAdapter(baseAdapter);
baseAdapter.notifyDataSetChanged();
if(baseAdapter.getCount()==0){
//無歷史搜索記錄
search_history_view.setVisibility(View.GONE);
}
}
/**
* 檢查數(shù)據(jù)庫中是否已經(jīng)有該條記錄
*/
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();
}
/**
* 清空數(shù)據(jù)
*/
private void deleteData() {
db = helper.getWritableDatabase();
db.execSQL("delete from history");
db.close();
loge("搜索歷史數(shù)據(jù)刪除成功");
queryData("");
}
數(shù)據(jù)庫的操作網(wǎng)上很多教程和文章,這里就不多加解釋,主要說一下邏輯吧后众。首先,我們輸入關(guān)鍵詞搜索到相應(yīng)的目標(biāo)地點后,點擊回調(diào)中,即插入一條該地名的數(shù)據(jù)胀糜。當(dāng)然,為了防止重復(fù),我們需要判斷一下數(shù)據(jù)庫中是否已經(jīng)存在該數(shù)據(jù)颅拦。我們剛進(jìn)入搜索界面的時候,需要查詢數(shù)據(jù)庫中所有的數(shù)據(jù)并展示。
- 搜索歷史數(shù)據(jù)的展示和刪除:
展示的話沒什么太大的問題,一般采用listview展示并為item加上點擊事件就OK了,主要是要設(shè)置好展示數(shù)據(jù)和刷新數(shù)據(jù)的邏輯教藻。這里主要提一下如何實現(xiàn)下方"清除搜索歷史"的友好展示以及邏輯距帅。
如果有人問你如何快速實現(xiàn)listview下面的button依附效果,你會怎么回答?常見的回答一般有兩種:
1.在listview下面放一個button,然后外面套一層ScrollerView
2.使用listview的addFooterView()給其添加底部布局
雖然兩種方式很容易想到,但是對于我們這些新手來說,動手實現(xiàn)起來多少有些彎彎繞繞。比如第一種方式,我們需要考慮如何處理嵌套滑動的問題,至于第二種,無非是研究listview之footerview的用法,當(dāng)然,拋開各自的難點,第二種方式無非更加優(yōu)雅一些,所以,這里只討論如何使用該方式實現(xiàn)括堤。
首先看一下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);
便可以實現(xiàn),但是footerview的點擊事件如何獲取呢?很多人說直接用onItenClickListener()呀,但是,大家可以通過log或者toast看看,點擊footerview是否真的響應(yīng)了碌秸。答案是 - 并沒有。我們應(yīng)該盡量避免在onITemClickListener回調(diào)方法中實現(xiàn)footerview點擊事件,因為position并沒有變化,上限依舊是原來的adapter.getCount()悄窃。我們可以先禁止footerview在item中的點擊響應(yīng),即addFooterView()方法第三個參數(shù)設(shè)為false,然后給footerView單獨設(shè)置點擊事件:
footerView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
deleteData();
toast("清除成功");
}
});
到這里,我們就完整實現(xiàn)了地圖的搜索功能了,雖然實現(xiàn)的方式比較簡單,但還是學(xué)到一些東西的讥电。后面有時間會將該部分功能做個demo單獨分享出來。另外大家如果有什么問題和優(yōu)化建議,歡迎留言反饋,不勝感激??广匙。
最后,請教一下大家mac如何錄制gif呢,為什么我用licecap錄制出來的是黑屏呢(⊙﹏⊙).