本篇將接續前一篇 得到的活動資料。目標為,實作會自動偵測使用者輸入,並顯示地點提示的搜尋框。在使用者按下搜尋框的提示後,畫面轉換到Map Activity並標記了剛剛選擇的提示地點,然後將之前得到的所有活動地點也顯示在Map上給使用者參考。
本篇使用Google Maps Android API第2版,基本的使用教學可以參考Tony的地圖與定位 1-3 (Maps and Positioning 1-3) ,我只記錄我在實作過程中,容易出錯的地方,還有大方向的開發流程。
1. API KEY
(1) 申請API key並啟用google map服務
申請的網址可以參考這裡 ,申請完後別忘記為你的應用程式啟用需要的API,如果沒有如下圖一樣啟用Google map APIs,在Android呼叫時service會沒有回應。
如果一開始就確定會用到Google map SDK,建議剛開始建立專案時就選擇Google Map Activity,就不用再打理麻煩的權限設定等…只要引入API key就好了,如下圖。
(2) 別忘記下載Google Play Service SDK
Google Map API為裡面的一部分。其實就算你沒有下載,gradle也會自動提醒你,這就是自動化工具的好處之一。
(3) 放置API Key的地方
將google map API key放到AndroidManifest.xml,你可能會注意到有2種Key的宣告,分別是com.google.android.maps.v2.API_KEY和com.google.android.geo.API_KEY。那要用哪一種?其實二種key的值都一樣,而com.google.android.maps.v2.API_KEY只有提供Map API service,com.google.android.geo.API_KEY則是Map API service和Place API service都有提供,考量到2個API key不能同時宣告,強烈推薦直接使用com.google.android.geo.API_KEY。
2. 第一步:AutoCompleteTextView與Google Place的結合
思路
使用AutoCompleteTextView元件,讓使用者在輸入地方資訊時,自動根據使用者輸入的內容提供地點提示讓使用者選擇。並在使用者選擇地點後,將之引導到標註了剛剛搜尋的地點的Map Activity,如下圖。
(1) Google官方範例參考:android-play-places的PlaceComplete專案
(2) AutoCompleteTextView元件使用介紹
AutoCompleteTextView實際上就是TextView的一種,只是他多增加一種功能,會根據使用者輸入的內容提供選項讓使用者選擇。增加提示的方法和填充ListView內文的方法相同,為它設置一個Adapter,裡面封裝了要提供給AutoCompleteTextView的提示文字即可,下面做個最簡單的java code示範,AutoCompleteTextView是Android SDK內建的View元件,可以在xml佈局時直接從UI Design mode內拖拉。
public class AutoCompleteTextViewActivity extends Activity {
String [] samples = new String [] { "One" , "Two" , "Three" , "Four" };
@Override
public void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . autocompletetextview );
ArrayAdapter < String > aa = new ArrayAdapter < String >( this ,
android . R . layout . simple_dropdown_item_1line , samples ); //建立Adapter
AutoCompleteTextView actv = ( AutoCompleteTextView ) findViewById ( R . id . auto ); //得到View物件
actv . setAdapter ( aa ); //設置Adapter
}
}
其中的ArrayAdapter,有實作Filterable interface。Filterable interface的功能是用來,從現有的全部結果中,依照你輸入的字元,過濾出符合的結果。而AutoCompleteTextView,則只單純地列出Filterable所傳回的結果。例如,當你輸入個B字元時,AutoCompleteTextView就藉由Filterable的函式,列出只有B開頭的國家。
(3) Google Example code提供的PlaceAutocompleteAdapter.java
PlaceAutocompleteAdapter繼承了ArrayAdapter,並實作了Filterable的Filter method,回傳的Filter功能是把使用者輸入的關鍵字,丟到Google Map service搜尋符合的結果,再將結果傳回來顯示在AutoCompleteTextView的列表提示,下面只截取重要PlaceAutocompleteAdapter.java的Filterable實作部分。
@Override
public Filter getFilter () {
Filter filter = new Filter () {
@Override
protected FilterResults performFiltering ( CharSequence constraint ) { //實作過濾功能
FilterResults results = new FilterResults ();
// 如果AutoCompleteTextView內無輸入任合內容則略過過濾
if ( constraint != null ) {
// 由讀者輸入的關鍵字constraint尋找提示
mResultList = getAutocomplete ( constraint );
if ( mResultList != null ) { //如果結果回傳成功
results . values = mResultList ;
results . count = mResultList . size ();
}
}
return results ;
}
@Override
protected void publishResults ( CharSequence constraint , FilterResults results ) {
if ( results != null && results . count > 0 ) {
// 回傳的提示至少一個以上才會通知更新
notifyDataSetChanged ();
} else {
notifyDataSetInvalidated ();
}
}
};
return filter ;
}
讓我們來看看getAutocomplete(CharSequence constaint)裡,如何使用Google Map Places service補充AutoCompleteTextView的提示。
private ArrayList < PlaceAutocomplete > getAutocomplete ( CharSequence constraint ) {
if ( mGoogleApiClient . isConnected ()) {
Log . i ( TAG , "Starting autocomplete query for: " + constraint );
// 輸入使用者輸入的關鍵字,回傳的PendingResult為AutocompletePredictionBuffer物件的List,內為Google Map Places service搜尋的結果
//mBounds為地點的搜索範圍(本App限台灣)
PendingResult < AutocompletePredictionBuffer > results =
Places . GeoDataApi
. getAutocompletePredictions ( mGoogleApiClient , constraint . toString (),
mBounds , mPlaceFilter );
// 等待回傳所有的地點結果,並最多等待60秒
AutocompletePredictionBuffer autocompletePredictions = results
. await ( 60 , TimeUnit . SECONDS );
// 確認結果回傳成功,否則回傳null
final Status status = autocompletePredictions . getStatus ();
if (! status . isSuccess ()) {
Toast . makeText ( getContext (), "Error contacting API: " + status . toString (),
Toast . LENGTH_SHORT ). show ();
Log . e ( TAG , "Error getting autocomplete prediction API call. " );
autocompletePredictions . release ();
return null ;
}
Log . i ( TAG , "Query completed Received ." );
//將結果的地點們(autocompletePredictions)轉移至ArrayList<PlaceAutocomplete>。
Iterator < AutocompletePrediction > iterator = autocompletePredictions . iterator (); //得迭代器
ArrayList resultList = new ArrayList <>( autocompletePredictions . getCount ());
while ( iterator . hasNext ()) {
AutocompletePrediction prediction = iterator . next ();
// PlaceAutocomplete為beans,內只封裝地點的ID和Description
resultList . add ( new PlaceAutocomplete ( prediction . getPlaceId (),
prediction . getDescription ()));
}
// 要釋放AutocompletePredictionBuffer物件,以預防記憶體流失
autocompletePredictions . release ();
return resultList ;
}
Log . e ( TAG , "Google API client is not connected for autocomplete query." );
return null ;
}
上面提到的Place bean,PlaceAutocomplete我就不贅述了,各位可以在這裡看到源碼 。
(4) 使用PlaceAutocompleteAdapter的AutoCompleteTextView
以下為引用PlaceAutocompleteAdapter的程式部分。
@Override
public void onActivityCreated ( Bundle savedInstanceState ) {
super . onActivityCreated ( savedInstanceState );
mAutocompleteView = ( AutoCompleteTextView )
getView (). findViewById ( R . id . autocomplete_places );
mAutocompleteView . setOnItemClickListener ( mAutocompleteClickListener ); //按下提示後呼叫mAutocompleteClickListener
mAdapter = new PlaceAutocompleteAdapter ( getActivity (), android . R . layout . simple_list_item_1 ,
mGoogleApiClient , BOUNDS_GREATER_SYDNEY , null ); //實體化PlaceAutocompleteAdapter
mAutocompleteView . setAdapter ( mAdapter ); //設定Adapter
}
以下說明PlaceAutocompleteAdapter的各個參數。
* mGoogleApiClient: 初始化google map client(使用Google Map API皆需要)
GoogleApiClient mGoogleApiClient = new GoogleApiClient . Builder ( getActivity ())
. enableAutoManage ( getActivity (), 0 /* clientId */ , this )
. addApi ( Places . GEO_DATA_API )
. build (); //set google map client init
* BOUNDS_GREATER_SYDNEY: 限制地點提示的範圍為台灣本島
private static final LatLngBounds BOUNDS_GREATER_SYDNEY = new LatLngBounds (
new LatLng ( 21.715956 , 119.419628 ), new LatLng ( 25.371160 , 122.138744 )); //Taiwan scope
範圍的選擇如下,由左至右;下至上。
(5) 使用AutoCompleteTextView的setOnItemClickListener method達到開啟Map Activity的目的
private AdapterView . OnItemClickListener mAutocompleteClickListener
= new AdapterView . OnItemClickListener () {
@Override
public void onItemClick ( AdapterView <> parent , View view , int position , long id ) {
final PlaceAutocompleteAdapter . PlaceAutocomplete item = mAdapter . getItem ( position ); //拿到place bean
final String placeId = String . valueOf ( item . placeId );
Log . i ( "Input place" , "Autocomplete item selected: " + item . description );
/*
使用placeId當參數,像Places Geo Data API要求PlaceBuffer物件(位置詳細資訊)
*/
PendingResult < PlaceBuffer > placeResult = Places . GeoDataApi
. getPlaceById ( mGoogleApiClient , placeId );
//當placeResult回傳結果後,呼叫mUpdatePlaceDetailsCallback method
placeResult . setResultCallback ( mUpdatePlaceDetailsCallback );
Log . i ( "Input place" , "Called getPlaceById to get Place details for " + item . placeId );
}
};
在mUpdatePlaceDetailsCallback method內,我們必須開啟新的Activity(Map Activity),並將位置資訊傳到新Activity,讓其地點標記在Map上。
private ResultCallback < PlaceBuffer > mUpdatePlaceDetailsCallback
= new ResultCallback < PlaceBuffer >() {
@Override
public void onResult ( PlaceBuffer places ) {
if (! places . getStatus (). isSuccess ()) { //如果回傳失敗
Log . e ( "TAG" , "Place query did not complete. Error: " + places . getStatus (). toString ());
places . release (); //釋放PlaceBuffer,以預防記憶體流失
return ;
}
//第一個結果
final Place place = places . get ( 0 );
Intent info = new Intent ( getActivity () , MapsActivity . class ); //Intent指向MapsActivity
info . putExtra ( "latitude" , place . getLatLng (). latitude ); //儲存緯度資料
info . putExtra ( "longitude" , place . getLatLng (). longitude ); //儲存經度資料
info . putExtra ( "name" , place . getName ()); //儲存地點名稱
Log . i ( "place" , place . getLatLng (). toString ());
getActivity (). startActivity ( info ); //開啟新Activity
places . release (); //釋放PlaceBuffer,以預防記憶體流失
}
};
這樣一整套思路就完成了!接下來將進入顯示活動標記。
##3. 第二步:標記剛剛的提示,並將多個活動位置顯示在Google Map上
(1) 標記前一個Activity所選擇的提示地點,並顯示在Map上
Intent info = getIntent ();
LatLng place = new LatLng ( info . getDoubleExtra ( "latitude" , 0 ), info . getDoubleExtra ( "longitude" , 0 ));
String name = info . getStringExtra ( "name" ));
mMap . addMarker ( new MarkerOptions (). position ( place ). title ( name )). showInfoWindow (); //在Mapp加入marker
mMap . moveCamera ( CameraUpdateFactory . newLatLngZoom ( place , 15.0f )); //移動Map鏡頭,關注在marker所在地,縮放程度為15f
CameraUpdate zoom = CameraUpdateFactory . zoomTo ( 14 f ); //縮放程度,此為主
mMap . animateCamera ( zoom , 1000 , new GoogleMap . CancelableCallback () { //1000 is animate velocity
@Override
public void onFinish () { //鏡頭更新成功後
Log . i ( "animateCamera" , "onFinish" );
markers = getEventAddress ();
for ( int i = 0 ; i < markers . size (); i ++) {
new GeocoderTask (). execute ( markers . get ( i )); //執行非同步,並傳入參數MarkerItem物件
}
}
@Override
public void onCancel () {
}
});
上面的getEventAddress()會將所有原來只有地址的活動,封裝成MarkerItem物件,MarkerItem內封裝活動的順序,地址和Address物件,MarkerItem bean就不在下面贅述,code 可以在這裡觀看。
public List < MarkerItem > getEventAddress () {
HashMap events = AppController . getInstance (). getEventbeans ();
List < xmlEventBean > ev = ( List < xmlEventBean >) events . get ( "KKTIX" ); //拿到所有活動
Log . i ( "getTimeANDplace" , "1: " + ev . size ());
List < MarkerItem > address = new ArrayList < MarkerItem >();
int i = 0 ;
for ( xmlEventBean e : ev ) {
//以下做切字,切出活動地點的地址,並加入List<MarkerItem>
if (! e . getTitle (). contains ( "測試" )) {
String [] arrays = e . getTimeANDplace (). split ( "/ +" );
if ( arrays . length >= 2 && ! arrays [ 1 ]. equals ( "無" )) {
if (! arrays [ 1 ]. contains ( " " )) {
Log . i ( "getTimeANDplace:arrays" , arrays [ 1 ]);
address . add ( new MarkerItem ( i , arrays [ 1 ], null ));
} else {
String [] ans = arrays [ 1 ]. split ( " " );
if ( ans . length >= 1 ) {
Log . i ( "getTimeANDplace:ans" , ans [ 0 ]);
address . add ( new MarkerItem ( i , ans [ 0 ], null ));
}
}
}
}
i ++;
}
Log . i ( "getTimeANDplace" , "2: " + address . size ()); //因為上面做了篩選,數量會比1:時來的少
return address ;
}
(2) 使用GeocoderTask extends AsyncTask執行非同步(多線程),實現“標注活動地點於地圖上”的工作
private class GeocoderTask extends AsyncTask < MarkerItem , Void , MarkerItem > {
@Override
protected MarkerItem doInBackground ( MarkerItem ... locationName ) { //在另一個Thread處理(非同步處)
//實體化Geocoder物件,他有“從經緯度尋找地址”和“從地址尋找經緯度”這2個功能
Geocoder geocoder = new Geocoder ( getBaseContext ());
List < Address > addresses = null ;
MarkerItem name = locationName [ 0 ]; //拿到MarkerItem物件
try {
// 使用MarkerItem物件所封裝的活動地址取得Address物件
// 並只取回傳結果的第一個Address物件放進MarkerItem物件
addresses = geocoder . getFromLocationName ( name . getAddress (), 1 );
if ( addresses != null && addresses . size () >= 1 ) {
name . setPlaceItem ( addresses . get ( 0 ));
}
} catch ( IOException e ) {
e . printStackTrace ();
}
return name ; 回傳 MarkerItem 物件至 onPostExecute
}
@Override
protected void onPostExecute ( MarkerItem addresses ) { //回到UI Thread執行,不能放耗時操作
if ( addresses == null || addresses . getPlaceItem () == null ) {
Log . i ( "FindNoLocation" , "on " + addresses . getAddress ());
}
// 從傳入的MarkerItem取得Address物件
Address address = ( Address ) addresses . getPlaceItem ();
// 如果Address不為空,實體化MarkerOptions,標記活動地點於map上
if ( address != null ) {
LatLng latLng = new LatLng ( address . getLatitude (), address . getLongitude ());
markerOptions = new MarkerOptions (). icon ( BitmapDescriptorFactory . fromResource ( R . drawable . direction_down )); //指定客製化marker icon
markerOptions . position ( latLng );
mMap . addMarker ( markerOptions . title ( addresses . getAddress ())); //作為marker query之用
}
}
}
從上面程式來看,為何要設定每個Marker的title?其實是為了之後OnMarkerClickListener所用,請看下面程式。
mMap . setOnMarkerClickListener ( new GoogleMap . OnMarkerClickListener () {
@Override
public boolean onMarkerClick ( Marker arg0 ) {
if ( markers != null ){
for ( MarkerItem it : markers ) {
//為了得知現在所選的Marker是哪個活動地點,一個個測試其活動地址是否符合
if ( arg0 . getTitle (). equals ( it . getAddress ())) {
if ( preClicked != null ){
preClicked . setIcon ( BitmapDescriptorFactory . fromResource ( R . drawable . direction_down ));
}
Log . i ( "clickedIcon" , it . getAddress ());
arg0 . setIcon ( BitmapDescriptorFactory . fromResource ( R . drawable . direction_down2 ));
if ( markinfo . getVisibility ()== View . GONE ) {
markinfo . setVisibility ( View . VISIBLE ); //如果細節活動框不可見,則顯示細節框
}
//設定活動細節框內容
infotitle . setText ( it . getBean (). getTitle ());
infodes . setText ( it . getBean (). getTimeANDplace ());
preClicked = arg0 ;
}
}
return true ;
} else {
return false ;
}
}
});
以下,為以上活動標記實作成品。
##4. project repo
git clone下面專案到自己的電腦後,使用 git checkout e687 .。