3.1 问题
除了将指定的位置显示在地图的中心,应用程序还需要在该位置上加上标记,以使其更加醒目。
3.2 解决方案
(API Level 9)
向地图添加Marker对象以及Circle和Polygon等形状元素。Marker对象是通过图标定义的交互式对象,该图标显示在给定位置。该位置可以是固定的,也可以设置Marker为可由用户拖动到他们希望的任意一点。每个Marker还可以响应触摸事件,如点击和长按。此外,可以为Marker提供包括标题的元数据和文本片段,当点击标记时会在弹出信息窗口中显示这些信息。这些窗口自身也可以定制显示。
Maps v2还支持绘制离散形状元素。这些元素在本质上是不可交互的,但我们会看到,可以轻松地添加与形状交互的功能。此功能也可以用于在地图上使用Polyline形状绘制路线,其不像其他选项一样会尝试绘制闭合的、填充的形状。
要点:
Google Maps v2是作为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()方法。