3.在地圖上標記位置
3.1 問題
除了將指定的位置顯示在地圖的中心,應用程式還需要在該位置上加上標記,以使其更加醒目。
3.2 解決方案
(API Level 9)
向地圖新增Marker物件以及Circle和Polygon等形狀元素。Marker物件是通過圖示定義的互動式物件,該圖示顯示在給定位置。該位置可以是固定的,也可以設定Marker為可由使用者拖動到他們希望的任意一點。每個Marker還可以響應觸控事件,如點選和長按。此外,可以為Marker提供包括標題的元資料和文字片段,當點選標記時會在彈出資訊視窗中顯示這些資訊。這些視窗自身也可以定製顯示。
Maps v2還支援繪製離散形狀元素。這些元素在本質上是不可互動的,但我們會看到,可以輕鬆地新增與形狀互動的功能。此功能也可以用於在地圖上使用Polyline形狀繪製路線,其不像其他選項一樣會嘗試繪製閉合的、填充的形狀。
要點:
Google Maps v2是作為Google+Play/">Google Play Services庫的一部分進行分發的,它在任意平臺級別都不是原生SDK的一部分。然而,目標平臺為API Level 9或以後版本的應用程式以及Google Play體系內的裝置都可以使用此繪相簿。
3.3 實現機制
顯示上一節的地圖應用程式,其中使用標記添加了一些感興趣的點。
以下兩段程式碼清單顯示了新的Activity示例,其中向地圖添加了一些標記。XML佈局與前一節中的相同,因此我們不會花費時間再次剖析其組成部分,只是為了完整性而在此新增此佈局。
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:text="Map Of Your Location" /> <RadioGroup android:id="@+id/group_maptype" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <RadioButton android:id="@+id/type_normal" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Normal Map" /> <RadioButton android:id="@+id/type_satellite" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Satellite Map" /> </RadioGroup> <fragment class="com.google.android.gms.maps.SupportMapFragment" android:id="@+id/map" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
顯示帶有標記的地圖的Activity
public class MarkerMapActivity extends FragmentActivity implements RadioGroup.OnCheckedChangeListener, GoogleMap.OnMarkerClickListener, GoogleMap.OnMarkerDragListener, GoogleMap.OnInfoWindowClickListener, GoogleMap.InfoWindowAdapter { private static final String TAG = "AndroidRecipes"; private SupportMapFragment mMapFragment; private GoogleMap mMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //檢查play services是否啟用且為最新版本 int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); switch (resultCode) { case ConnectionResult.SUCCESS: Log.d(TAG, "Google Play Services is ready to go!"); break; default: showPlayServicesError(resultCode); return; } mMapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.map); mMap = mMapFragment.getMap(); // 監控與標記元素的互動 mMap.setOnMarkerClickListener(this); mMap.setOnMarkerDragListener(this); // 設定應用程式以服務資訊視窗的檢視 mMap.setInfoWindowAdapter(this); // 監控資訊視窗上的點選事件 mMap.setOnInfoWindowClickListener(this); // Google 總部 ( 37.427,-122.099) Marker marker = mMap.addMarker(new MarkerOptions() .position(new LatLng(37.4218, -122.0840)) .title("Google HQ") // 將來自應用程式的影象資源顯示為標記 .icon(BitmapDescriptorFactory .fromResource(R.drawable.logo)) //降低透明度 .alpha(0.6f)); //使此標記在地圖上可拖動 marker.setDraggable(true); // 減去 0.01 度 mMap.addMarker(new MarkerOptions() .position(new LatLng(37.4118, -122.0740)) .title("Neighbor #1") .snippet("Best Restaurant in Town") // 以預設顏色顯示預設標記 .icon(BitmapDescriptorFactory.defaultMarker())); // 增加 0.01 度 mMap.addMarker(new MarkerOptions() .position(new LatLng(37.4318, -122.0940)) .title("Neighbor #2") .snippet("Worst Restaurant in Town") // 使用淺藍色顯示預設標記 .icon(BitmapDescriptorFactory .defaultMarker(BitmapDescriptorFactory.HUE_AZURE))); // 居中地圖並同時縮放 LatLng mapCenter = new LatLng(37.4218, -122.0840); CameraUpdate newCamera = CameraUpdateFactory .newLatLngZoom(mapCenter, 13); mMap.moveCamera(newCamera); // 連線地圖型別選擇器UI RadioGroup typeSelect = (RadioGroup) findViewById(R.id.group_maptype); typeSelect.setOnCheckedChangeListener(this); typeSelect.check(R.id.type_normal); } /** OnCheckedChangeListener方法 */ @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.type_satellite: mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); break; case R.id.type_normal: default: mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); break; } } /** OnMarkerClickListener方法 */ @Override public boolean onMarkerClick(Marker marker) { // 返回 true 以禁用自動居中和資訊彈出視窗 return false; } /** OnMarkerDragListener 方法 */ @Override public void onMarkerDrag(Marker marker) { // 在標記移動時執行某些操作 } @Override public void onMarkerDragEnd(Marker marker) { Log.i("MarkerTest", "Drag " + marker.getTitle() + " to " + marker.getPosition()); } @Override public void onMarkerDragStart(Marker marker) { Log.d("MarkerTest", "Drag " + marker.getTitle() + " from " + marker.getPosition()); } /** OnInfoWindowClickListener 方法 */ @Override public void onInfoWindowClick(Marker marker) { // 操作選擇事件,在此僅是關閉視窗 marker.hideInfoWindow(); } /** InfoWindowAdapter 方法 */ /* * 返回將放在標準資訊視窗內的內容檢視 * 僅在getInfoWindow() 返回null時呼叫 */ @Override public View getInfoContents(Marker marker) { //在此改為嘗試返回 createInfoView() return null; } /* * 返回整個待顯示的資訊視窗 */ @Override public View getInfoWindow(Marker marker) { View content = createInfoView(marker); content.setBackgroundResource(R.drawable.background); return content; } /* * 用於構造內容檢視的私有輔助方法 */ private View createInfoView(Marker marker) { // 我們沒有父物件用於佈局,因此傳遞null View content = getLayoutInflater().inflate( R.layout.info_window, null); ImageView image = (ImageView) content .findViewById(R.id.image); TextView text = (TextView) content .findViewById(R.id.text); image.setImageResource(R.drawable.ic_launcher); text.setText(marker.getTitle()); return content; } /* *當 Play Services缺失或缺失不對時, * 客戶端將以對話方塊的形式幫助使用者進行更新。 */ private void showPlayServicesError(int errorCode) { // 從Google Play services 獲得錯誤對話方塊 Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog( errorCode, this, 1000 /* RequestCode */); // 如果Google Play services 可以提供錯誤對話方塊 if (errorDialog != null) { // 為錯誤對話方塊建立新的 DialogFragment SupportErrorDialogFragment errorFragment = SupportErrorDialogFragment.newInstance(errorDialog); // 在 DialogFragment 中顯示錯誤對話方塊 errorFragment.show( getSupportFragmentManager(), "Google Maps"); } } }
免責宣告:
我們沒有實際拜訪此地圖上的這些位置以瞭解是否有餐館,也沒有了解這些餐館的客戶評級是否符合我們在此放置的副標題!
我們向Activity添加了一些新的偵聽器介面,該Activity現在設定為監控每個Marker上的點選拖動事件,並且監控通過點選Marker顯示的彈出資訊視窗中的點選事件。此外,我們實現了InfoWindowAdapter,它用於最終定製彈出視窗,但目前先不討論該介面卡。
將MarkerOptions例項傳入GoogleMap.addMarker(),這樣就可以向地圖新增標記。
MarkerOptions的工作方式類似於生成器,它只是在建構函式中將想要應用的所有信息連結在一起(我們以及完成該工作)。在MarkerOptions中設定一些基本資訊,如標記位置、顯示圖示和標題。還有一些用於修該標記顯示的額外選項,如alpha()、旋轉和錨點。我們選擇在位於山景城(Mountain View)的Google總部新增一個標記,並且在其附近新增另外兩個標記。
有許多支援方法可用於建立Marker圖示,使用BitmapDescriptor物件可應用這些方法,BitmapDescriptorFactory則提供了建立所有元素的方法。對於我們的兩個元素,在此選擇了defaultMarker()方法,該方法建立標準的Google大頭針圖示進行顯示。我們還可以傳入幾個常量之一來控制大頭針圖示的顯示顏色。
我們對位於Google總部的標記進行了控制,使用fromResource()將其定義為應用程式中已有的圖示。還可以使用單獨的工廠方法應用可能位於資源目錄中的影象。此外,我們將此標記設定為可由使用者拖動。這意味著如果使用者長按大頭針圖示,則會從其當前位置拾起該圖示,將其拖放到地圖上的任意位置。我們實現的OnMarkerDragListener提供了關於二如何放置標記的回撥。
如果使用者點選某個標記,標準資訊視窗將顯示在圖示上方。該視窗顯示應用於標記的標題和程式碼片段。我們實現了OnInfoWindowClickListener,在點選視窗時將其關閉,這不是預設行為。
注意,我們不需要實現OnMarkerClickListener來實現在此描述的行為,但我們可以重寫該行為。預設情況下,資訊視窗將顯示,並且地圖將在所選標記出居中。如果onMarkerClick()返回true,則可以禁用此行為並提供我們自己的行為。
1.定製資訊視窗
為了幫助你瞭解如何定製在點選標記時彈出的資訊視窗,接下來為視窗新增一些自定義UI(參見以下兩段程式碼),並且修改在Activity中實現的InfoWindowAdapter方法。
res/layout/info_window.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" > <ImageView android:id="@+id/image" android:layout_width="35dp" android:layout_height="35dp" android:layout_gravity="center_horizontal" android:scaleType="fitCenter" /> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
res/drawable/background.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:radius="10dp"/> <solid android:color="#CCC"/> <padding android:left="10dp" android:right="10dp" android:top="10dp" android:bottom="10dp"/> </shape>
通過從getInfoContents()返回有效的檢視,該檢視就會用作標準視窗背景顯示中的內容。從getInfoWindow()返回相同的檢視,該檢視會顯示為沒有標準組件的完全自定義的視窗。我們已將彈出視窗的建立過程抽象化到一個輔助方法中,因此可以放鬆嘗試上訴兩種方式。
2.操作形狀
接下來討論像地圖新增形狀元素。在下面的示例中,我們建立了名為ShapeAdapter的自定義類,該類建立圓形或矩形形狀並將它們新增到地圖上,用於描述地圖地區。
該例也使用Google Map 的onMapClickListener驗證使用者何時點選某個地區進行選擇。以下清單程式碼顯示了ShapeAdapter的程式碼。
建立地圖形狀的ShapeAdapter
public class ShapeAdapter implements OnMapClickListener { private static final float STROKE_SELECTED = 6.0f; private static final float STROKE_NORMAL = 2.0f; /* 所繪製區域的顏色 */ private static final int COLOR_STROKE = Color.RED; private static final int COLOR_FILL = Color.argb(127, 0, 0, 255); /* * 外部介面,用於通知偵聽器基於使用者點選的所選區域進行變化 */ public interface OnRegionSelectedListener { //使用者選擇了跟蹤地區 public void onRegionSelected(Region selectedRegion); //使用者選擇了沒有地區的區域 public void onNoRegionSelected(); } /* * 地圖上互動式地區的基礎定義 * 定義方法以更改顯示並檢查使用者點選 */ public static abstract class Region { private String mRegionName; public Region(String regionName) { mRegionName = regionName; } public String getName() { return mRegionName; } //檢查位置是否在此地區內 public abstract boolean hitTest(LatLng point); //根據使用者的選擇更改地區的顯示 public abstract void setSelected(boolean isSelected); } /* *將地區繪製為圓形 */ private static class CircleRegion extends Region { private Circle mCircle; public CircleRegion(String name, Circle circle) { super(name); mCircle = circle; } @Override public boolean hitTest(LatLng point) { final LatLng center = mCircle.getCenter(); float[] result = new float[1]; Location.distanceBetween(center.latitude, center.longitude, point.latitude, point.longitude, result); return (result[0] < mCircle.getRadius()); } @Override public void setSelected(boolean isSelected) { mCircle.setStrokeWidth(isSelected ? STROKE_SELECTED : STROKE_NORMAL); } } /* * 將地區繪製為矩形 */ private static class RectRegion extends Region { private Polygon mRect; private LatLngBounds mRectBounds; public RectRegion(String name, Polygon rect, LatLng southwest, LatLng northeast) { super(name); mRect = rect; mRectBounds = new LatLngBounds(southwest, northeast); } @Override public boolean hitTest(LatLng point) { return mRectBounds.contains(point); } @Override public void setSelected(boolean isSelected) { mRect.setStrokeWidth(isSelected ? STROKE_SELECTED : STROKE_NORMAL); } } private GoogleMap mMap; private OnRegionSelectedListener mRegionSelectedListener; private ArrayList<Region> mRegions; private Region mCurrentRegion; public ShapeAdapter(GoogleMap map) { //在內部跟蹤地區以確認選擇 mRegions = new ArrayList<Region>(); mMap = map; mMap.setOnMapClickListener(this); } public void setOnRegionSelectedListener(OnRegionSelectedListener listener) { mRegionSelectedListener = listener; } /* * 圍繞給定點構造並新增新的圓形地區 */ public void addCircularRegion(String name, LatLng center, double radius) { CircleOptions options = new CircleOptions() .center(center) .radius(radius); //設定形狀的顯示屬性options.strokeWidth(STROKE_NORMAL).strokeColor(COLOR_STROKE).fillColor(COLOR_FILL); Circle c = mMap.addCircle(options); mRegions.add(new CircleRegion(name, c)); } /* * 使用給定邊界構造並新增新的矩形地區 */ public void addRectangularRegion(String name, LatLng southwest, LatLng northeast) { PolygonOptions options = new PolygonOptions().add( new LatLng(southwest.latitude, southwest.longitude), new LatLng(southwest.latitude, northeast.longitude), new LatLng(northeast.latitude, northeast.longitude), new LatLng(northeast.latitude, southwest.longitude)); //設定形狀的顯示屬性options.strokeWidth(STROKE_NORMAL).strokeColor(COLOR_STROKE).fillColor(COLOR_FILL); Polygon p = mMap.addPolygon(options); mRegions.add(new RectRegion(name, p, southwest, northeast)); } /* * 處理從地圖物件傳入的觸控事件 * 確定可能選擇了哪個地區元素 *如果多個地區在此地區重疊,則會選擇新增的第一個地區 */ @Override public void onMapClick(LatLng point) { Region newSelection = null; //查詢並選擇觸控的地區 for (Region region : mRegions) { if (region.hitTest(point) && newSelection == null) { region.setSelected(true); newSelection = region; } else { region.setSelected(false); } } if (mCurrentRegion != newSelection) { //通知並更新改動 if (newSelection != null && mRegionSelectedListener != null) { mRegionSelectedListener.onRegionSelected(newSelection); } else if (mRegionSelectedListener != null) { mRegionSelectedListener.onNoRegionSelected(); } mCurrentRegion = newSelection; } } }
該類定義了名為Region的抽象型別,我們可以使用它定義形狀型別之間的常見模式。首先,每個地區必須定義地圖位置是否在給定地區內的邏輯,以及在選擇地區時執行哪些操作。然後,為Circle和Polygon形狀定義此邏輯的實現,或者用於繪製矩形。圓形地區由中心點和半徑定義,而矩形地區則由其西南角和東北角的點定義。我們構造矩形的方法是使用組成該形狀的4個角點座標構造Polygon。
觸控事件將由偵聽器介面的onMapClick()方法處理,而Maps庫提供了作為LatLng位置的觸控位置。只需要檢查中心點和觸控位置之間的距離是否大於半徑,我們就可以驗證這些事件在圓形地區內。Location有一個便利方法可計算兩個地圖點之間的直接距離。對於矩形地區,我們使用作為Maps庫一部分的LayLngBounds方法,因為它直接驗證給定點是在形狀的內部還是外部。
對於每個觸控事件,我們遍歷地區列表以查詢第一個可能包含此位置的地區。如果未找到任何地區,則將所選地區設定為null。接下來,確定選擇項是否已改變,並且呼叫自定義介面OnRegionSelectedListener的某個方法,較高階的物件可以使用該方法獲得這些事件的通知。
以下清單程式碼顯示瞭如何在Activity內部使用此介面卡。
整合了ShapeAdapter的Activity
public class ShapeMapActivity extends FragmentActivity implements RadioGroup.OnCheckedChangeListener, ShapeAdapter.OnRegionSelectedListener { private static final String TAG = "AndroidRecipes"; private SupportMapFragment mMapFragment; private GoogleMap mMap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //檢查Google play services 是否已啟用且為最新版本 int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this); switch (resultCode) { case ConnectionResult.SUCCESS: Log.d(TAG, "Google Play Services is ready to go!"); break; default: showPlayServicesError(resultCode); return; } mMapFragment = (SupportMapFragment) getSupportFragmentManager() .findFragmentById(R.id.map); mMap = mMapFragment.getMap(); ShapeAdapter adapter = new ShapeAdapter(mMap); adapter.setOnRegionSelectedListener(this); adapter.addRectangularRegion("Google HQ", new LatLng(37.4168, -122.0890), new LatLng(37.4268, -122.0790)); adapter.addCircularRegion("Neighbor #1", new LatLng(37.4118, -122.0740), 400); adapter.addCircularRegion("Neighbor #2", new LatLng(37.4318, -122.0940), 400); //居中地圖並同時縮放 LatLng mapCenter = new LatLng(37.4218,-122.0840); CameraUpdate newCamera = CameraUpdateFactory.newLatLngZoom(mapCenter, 13); mMap.moveCamera(newCamera); //連線地圖型別選擇器 UI RadioGroup typeSelect = (RadioGroup) findViewById(R.id.group_maptype); typeSelect.setOnCheckedChangeListener(this); typeSelect.check(R.id.type_normal); } /** OnCheckedChangeListener 方法 */ @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.type_satellite: mMap.setMapType(GoogleMap.MAP_TYPE_SATELLITE); break; case R.id.type_normal: default: mMap.setMapType(GoogleMap.MAP_TYPE_NORMAL); break; } } /** OnRegionSelectedListener 方法 */ @Override public void onRegionSelected(Region selectedRegion) { Toast.makeText(this, selectedRegion.getName(), Toast.LENGTH_SHORT).show(); } @Override public void onNoRegionSelected() { Toast.makeText(this, "No Region", Toast.LENGTH_SHORT).show(); } /* * 當 Play Services 缺失或版本不正確時,客戶端庫將顯示了一個對話方塊, * 幫助使用者進行更新 */ private void showPlayServicesError(int errorCode) { //獲得來自 Google Play services的錯誤對話方塊 Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog( errorCode, this, 1000 /* RequestCode */); // 如果 Google Play services 可以提供錯誤對話方塊 if (errorDialog != null) { // 為錯誤對話方塊建立新的 DialogFragment 可以提供錯誤對話方塊 SupportErrorDialogFragment errorFragment = SupportErrorDialogFragment.newInstance(errorDialog); // 在 DialogFragment中顯示錯誤對話方塊 errorFragment.show( getSupportFragmentManager(), "Google Maps"); } } }
在此添加了與前一個示例相同的位置,但這一次使用新的ShapeAdapter將其新增為形狀地區。將Google總部新增為矩形地區,而將其他兩個標記新增圓形地區。當用戶改變選擇並影響到任何上訴地區時,則會呼叫onRegionSelected()或onNoRegionSelected()方法。