Android地图—— Mapbox 10.3.0 聚类标签实现
Mapbox的初始化配置请见这个文章:Android地图—— Mapbox 10.3.0 接入与基础使用
我们知道,mapbox显示地图是利用多个 layers 组装而成,比如water layer等。mapbox支持我们添加更多的layer用于自定义用途,这里我们用layer来展示大量的标记。
以下例子实现了新建layer展示标记,并实现标记随地图缩放聚合、以及用户点击交互的功能。
具体的实现我参考了这个博主的代码:MapBoxMap 之 SymbolLayer实现标记聚合,在其基础上进行了10.3.0版本的适配,以及添加了点击标记的交互,这里的实现仅为个人尝试,可能有更好的实现方法。(注:由于官网已有kotlin的实现示例,这里将以java作为示例编写语言)
效果图先行:
以下文章主要为原理解释,非完整代码展示,建议搭配本文示例程序的代码食用:
1. 基础知识概念
首先明确以下几个概念:
Sources:
地图源,包含要添加到地图中的所有元素描述,以及他们的地理坐标信息。
主要参数:
id: sources的独特标识;
type: 当前sources的类别,类别决定了source中的元素是怎么样的。type的不同也决定了source应该包含什么其他参数;
关于source 不同type包含的参数说明:
https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/
Layers:
样式层,用于决定Sources中的元素在地图上的显示样式。
主要参数:
id: layers的独特标识;
type: layers的类别,不同的type有不同的样式展现形式。
source: 该layer是哪个源中的。
filter: Expression表达式,根据各种条件判断当前layer是否展示。
Layers的更多参数说明:
https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/
一个sources可以对应一个或多个layers,这取决于你想以多少种不同形式展示你的数据。
关于source和layers的官方解释:
https://docs.mapbox.com/android/maps/guides/styles/work-with-layers/
要实现一个具有聚合标记物功能,且标记物可自定义样式的图层,我们应该选择sources类型为geojson,layers类型我们则选择样式最丰富的symbol。
2. source初始化并添加到style中:
定义source-id:
private static final String CLUSTER_SOURCE_ID = "cluster-source-id";
初始化例子1:
例子1为在使用 mMapboxMap.loadStyleUri() 方法拿到加载完毕的 mStyle 后,再去添加 source 的方法。
以下代码我们在常规初始化的基础上添加了对 参数 clusterProperties 的设置(该参数可以自定义聚合标记物所包含的特征值,用于一些个性化设置),我们定义了一个 first_item_id 属性,保存了在聚合标记物对象集合中各标记物之间最小的id值,这是为了方便我们后面设置 聚合对象的显示 做准备。
//添加地图源 type:GeoJson
{
JSONObject clusterProperties = new JSONObject();
try {
clusterProperties.put("first_item_id",
new JSONArray(Expression.min(Expression.toNumber(Expression.get("id"))).toJson()));
} catch (JSONException e) {
e.printStackTrace();
}
Log.d(TAG, clusterProperties.toString());
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("type","geojson");
jsonObject.put("data",""); // 初始数据为空
jsonObject.put("cluster",true); // 启用聚合功能
jsonObject.put("clusterRadius",30);
jsonObject.put("clusterProperties",clusterProperties); // 聚合对象包含的特征
} catch (JSONException e) {
e.printStackTrace();
}
// Log.d(TAG, jsonObject.toString());
Expected<String, Value> out = Value.fromJson(jsonObject.toString());
Expected<String, None> success = mStyle.addStyleSource(CLUSTER_SOURCE_ID,
Objects.requireNonNull(out.getValue()));
Log.d(TAG,"[initClusterLayers]source create: " +
(success.isError() ? success.getError() : "success"));
}
初始化例子2:
例子1我们直接生成了一个关于 GenJsonSource 的JSON表达式,通过 style 的 addStyleSource() 方法添加到 style 中。
MapBox也提供了直接构造一个GenJson类型Source的方法:
GeoJsonSource geoJsonSource = new GeoJsonSource.Builder(CLUSTER_SOURCE_ID)
.data("")
.cluster(true)
.clusterRadius(30)
.build();
通过这个方法生成的 GenJsonSource 需要在载入地图style前,添加到 StyleExtensionImpl.Builder() 构建的 StyleExtension ,再通过 mMapboxMap.loadStyle() 方法传入这个 StyleExtension 对象去加载地图Style。
StyleExtensionImpl.Builder builder = new StyleExtensionImpl.Builder(DEFAULT_MAP_STYLE);
builder.addSource(geoJsonSource);
mMapboxMap.loadStyle(builder.build(), new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NotNull Style style) {
}
});
3. layer初始化并添加到style中:
layer表示数据的呈现形式,在这里我们定义三个类型的layer用于展示我们的数据:
- 单个标记物的icon展示layer
- 聚合对象(多个标记物的集合)的icon展示layer
- 聚合对象(多个标记物的集合)的text展示layer
如果一个对象为 聚合对象cluster ,其将包含以下几个属性:
除了上面四个默认属性,其还将包含你在 Source 创建时设置的 clusterProperties 中的属性值。
这里以单个标记物对象的展示的layer的初始化为例:
定义全局变量:
private static final String LAYER_ID_SINGLE_POINT = "layer-id-single-points";
private static final String POINT_COUNT = "point_count";
首先,不同的 Layer 应该在不同条件下显示(如我们上面定义的那样,单个标记物的icon展示的layer应该在标记物为 非聚合对象 的情况下展示),通过 Layer 的属性 filter,我们可以设置 Layer 的展示条件,该属性接受一个 Expression 表达式 作为输入。
根据上面所述,我们可以利用 cluster聚合对象 包含 ”point_count” 属性这一特征区分 聚合对象 和 单一对象。
如下代码所示,我们构造了一个判断当前对象是否有 ”point_count” 属性的 Express表达式 ,该表达式 当 当前对象 不含有 ”point_count” 属性时会返回 true。
该Expression的Json表达式为:
[“!”,[“has”,”point_count”]]
java构建Experssion表达式代码:
// 构造filter表达式
Expression.ExpressionBuilder filterExpression = new Expression.ExpressionBuilder("!");
filterExpression.has(POINT_COUNT);
上述构造的 expression 将传入 layer 的 filter 属性中,以控制该 layer 是否显示。
其次,这一层的标记物我们打算将以图像的形式展现,利用 symbol 中的 icon-image 属性即可实现,为了控制这个图像的显示特点,我们定义一个自定义的 iconLayout Json对象,来描述图像显示的属性:
JSONObject iconLayout = new JSONObject();
try {
iconLayout.put("icon-ignore-placement",true); // 设置其他symbol与icon碰撞时仍显示
iconLayout.put("icon-allow-overlap",true); // 设置icon与其他symbol碰撞时仍显示
} catch (JSONException e) {
e.printStackTrace();
}
icon-ignore-placement 等属性值为symbol标记类型所包含的属性,更多symbol特殊属性值可参照:
https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#symbol
最后Layer的构造代码所下所示:
其中 “{” + IMAGE_ID_SINGLE_POINT_ICON + "}” 表达式,可以获取到当前单一标记物对象中属性名为 IMAGE_ID_SINGLE_POINT_ICON 的属性值。
icon-image 属性接受该表达式,以在Style中检索 通过 style.addImage() 方法传入的 与之id相匹配的图片;这样即可对拥有不同id的标记,显示不同的图片。(向style中addImage可参看第5部分)
private static final String IMAGE_ID_SINGLE_POINT_ICON = "id";
// 构造layer
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("id", LAYER_ID_SINGLE_POINT);
jsonObject.put("type","symbol");
jsonObject.put("source", CLUSTER_SOURCE_ID);
jsonObject.put("icon-image","{" + IMAGE_ID_SINGLE_POINT_ICON + "}");
jsonObject.put("layout", iconLayout);
jsonObject.put("filter", new JSONArray(filterExpression.build().toJson()));
} catch (JSONException e) {
e.printStackTrace();
}
Expected<String, Value> out = Value.fromJson(jsonObject.toString());
Expected<String, None> success = mStyle.addStyleLayer(Objects.requireNonNull(out.getValue()),
new LayerPosition(null, null, null));
Log.d(TAG,"[initClusterLayers]single icon layer create: " +
(success.isError() ? success.getError() : "success"));
同 Source 的构造一样,我们也可以直接使用类 SymbolLayer 直接进行构造。但其也需要在Style加载前添加到 StyleExtension 中,这里不再举例。
其余layer的添加代码见文章开头的gitee库代码。
在示例的代码中,我们对 聚合对象(多个标记物的集合)的icon展示layer 这一类型的layer构建了三个,这样可以分别在聚合对象包含 标记物数量x,x>=150;150>x>=20; 20>x>1; 三种情况下定制不同的展示模式(但示例代码并没有赋予他们不同的展示,如果你也不需要对此进行区别展示,可以仅为聚合对象的icon创建一个 layer 即可)。
在上述区分情况下,filter 属性中的 Expression表达式 需有如下考虑:
- 当前展示对象为聚合对象;
- 当前聚合对象所包含的标记物数量在某区间内;
根据上面所示,我们 filter 的判断条件有多个,因此我们使用 all操作符 ,当任意一个条件不满足时就返回false,不进行显示。
首先,我们通过以下代码获取到显示对象中属性名为 ”point_count” 的属性值,并转为数字 (使用to-number操作符)。该属性值即表示该对象中包含的标记物数量。
Expression pointCount = Expression.toNumber(Expression.get(POINT_COUNT));
第一个条件:当前展示对象为聚合对象,即:要有”point_count”属性。
expressionBuilder.has(POINT_COUNT);
第二个条件:当前聚合对象所包含的标记物数量在某区间内(这里以x>=150为例)我们使用 gte操作符 (greater or equal)比较 ”point_count” 和150;
完整代码如下所示:
int[] layers = new int[]{150, 20, 1}; // 按照聚合对象中包含的标记物数量区分
for (int i = 0; i < layers.length; i++) {
Expression pointCount = Expression.toNumber(Expression.get(POINT_COUNT));
Expression.ExpressionBuilder expressionBuilder = new Expression.ExpressionBuilder("all");
int finalI = i;
expressionBuilder.has(POINT_COUNT);
if(finalI == 0) {
expressionBuilder.gte(expressionBuilder1 -> {
expressionBuilder1.addArgument(pointCount);
expressionBuilder1.literal(layers[finalI]);
return null;
});
} else {
expressionBuilder.gte(expressionBuilder12 -> {
expressionBuilder12.addArgument(pointCount);
expressionBuilder12.literal(layers[finalI]);
return null;
});
expressionBuilder.lt(expressionBuilder13 -> {
expressionBuilder13.addArgument(pointCount);
expressionBuilder13.literal(layers[finalI - 1]);
return null;
});
}
// ...
}
关于Expression操作符的更多说明可以参见:
https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions
4. 向Source中添加数据:
对于 genjson 类型的 source 而言,其中存储数据的为属性data,向 source 中添加数据即设置其属性值data。
data属性接受一个geojson对象。
GeoJSON 是一种对各种地理数据结构进行编码的格式。GeoJSON对象可以表示几何、特征或者特征集合。关于GeoJson的说明可以参见下面这个博主的文章:GeoJSON 学习
根据 GeoJSON 的数据组织格式,我们构建了一个自己的 GeoJSON类:
public class MapGeoJson {
/**
* GeoJSON是一种对各种地理数据结构进行编码的格式。GeoJSON对象可以表示几何、特征或者特征集合。
* 样例:https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson
* type : FeatureCollection
* crs : {"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}}
* features : [{"type":"Feature","properties":{"id":"ak16994521","mag":2.3,"time":1507425650893,"felt":null,"tsunami":0},"geometry":{"type":"Point","coordinates":[-151.5129,63.1016,0]}},{"type":"Feature","properties":{"id":"ak16994519","mag":1.7,"time":1507425289659,"felt":null,"tsunami":0},"geometry":{"type":"Point","coordinates":[-150.4048,63.1224,105.5]}},{"type":"Feature","properties":{"id":"ak16994517","mag":1.6,"time":1507424832518,"felt":null,"tsunami":0},"geometry":{"type":"Point","coordinates":[-151.3597,63.0781,0]}}]
*/
private final String type = "FeatureCollection"; // MapGeoJson类型
private final CrsBean crs = new CrsBean(); // [可选]坐标参考系
private List<FeaturesBean> features; // 特征对象的集合
public List<FeaturesBean> getFeatures() {
return features;
}
public void setFeatures(List<FeaturesBean> features) {
this.features = features;
}
public static class CrsBean {
/**
* 非空的CRS对象有两个强制拥有的对象:“type"和"properties”。
* type表明Crs对象类型(名字Crs 或者 链接Crs)
* type : name
* properties : {"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}
*/
private final String type = "name";
private final CrsProperties properties = new CrsProperties();
public static class CrsProperties {
/**
* 名字Crs的Properties必须为 name
* name : urn:ogc:def:crs:OGC:1.3:CRS84
*/
private final String name = "urn:ogc:def:crs:OGC:1.3:CRS84"; // 标识坐标参考系统的字符串
}
}
// 特征对象
public static class FeaturesBean {
/**
* 类型为"Feature"的GeoJSON对象是特征对象
* type : Feature
* properties : {"type":0,"id":"ak16994521","mag":2.3,"time":1507425650893,"felt":null,"tsunami":0}
* geometry : {"type":"Point","coordinates":[-151.5129,63.1016,0]}
*/
private final String type = "Feature";
private FeatureProperties properties;
private FeatureGeometry geometry;
public FeatureProperties getProperties() {
return properties;
}
public void setProperties(FeatureProperties properties) {
this.properties = properties;
}
public FeatureGeometry getGeometry() {
return geometry;
}
public void setGeometry(FeatureGeometry geometry) {
this.geometry = geometry;
}
/**
* 特征对象特征值
*/
public static class FeatureProperties {
private String id;
private String imageName;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getImageName() {
return imageName;
}
public void setImageName(String imageName) {
this.imageName = imageName;
}
}
/**
* 几何对象
* 包含 coordinates 数组表示几何位置
*/
public static class FeatureGeometry {
/**
* type : Point
* coordinates : [-151.5129,63.1016,0]
*/
private String type = "Point";
private List<Double> coordinates;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<Double> getCoordinates() {
return coordinates;
}
public void setCoordinates(List<Double> coordinates) {
this.coordinates = coordinates;
}
}
}
/**
* 生成Json字符串
*/
public String crateJson() {
Gson gson = new Gson();
return gson.toJson(this);
}
}
根据我们的集合数据构造一个 GeoJson 对象:
构造自定义的标记物对象PhotoAnnotation.java
/**
* 地图 照片类型 注释
*/
public class PhotoAnnotation {
private final String mId;
private final String mImageName;
private final double mLatitude; // 标记物坐标
private final double mLongitude;
public PhotoAnnotation(String id, String imageName, double latitude, double longitude) {
mId = id;
mImageName = imageName;
mLatitude = latitude;
mLongitude = longitude;
}
public String getId() {
return mId;
}
public double getLatitude() {
return mLatitude;
}
public double getLongitude() {
return mLongitude;
}
public String getImageName() {
return mImageName;
}
}
构造GeoJson对象工具类:
public class MapGeoJsonUtil {
public static MapGeoJson getMapGeoJsonFromList(List<PhotoAnnotation> list) {
if (list == null || list.size() <= 0){
return null;
}
MapGeoJson mapGeoJson = new MapGeoJson();
List<MapGeoJson.FeaturesBean> featureList = new ArrayList<>();
//填充数据
for (PhotoAnnotation photoAnnotation : list) {
MapGeoJson.FeaturesBean fBean = new MapGeoJson.FeaturesBean();
fBean.setProperties(getDevicePropertiesBeanX(photoAnnotation.getId(), photoAnnotation.getImageName()));
fBean.setGeometry(getGeometryBean(photoAnnotation.getLatitude(), photoAnnotation.getLongitude()));
featureList.add(fBean);
}
mapGeoJson.setFeatures(featureList);
return mapGeoJson;
}
private static MapGeoJson.FeaturesBean.FeatureGeometry getGeometryBean(double latitude, double longitude) {
MapGeoJson.FeaturesBean.FeatureGeometry geometryBean = new MapGeoJson.FeaturesBean.FeatureGeometry();
List<Double> coordinates = new ArrayList<>();
coordinates.add(longitude);
coordinates.add(latitude);
geometryBean.setCoordinates(coordinates);
return geometryBean;
}
private static MapGeoJson.FeaturesBean.FeatureProperties getDevicePropertiesBeanX(String id, String imageName) {
MapGeoJson.FeaturesBean.FeatureProperties pBeanX = new MapGeoJson.FeaturesBean.FeatureProperties();
pBeanX.setId(id);
pBeanX.setImageName(imageName);
return pBeanX;
}
}
通过以下代码即可生成Geojson对象:
MapGeoJson mapGeoJson = MapGeoJsonUtil.getMapGeoJsonFromList(photoAnnotationList);
这里的 photoAnnotationList 需要自行生成,这里提供一个例子:
例子中传入的imageName为项目assert中包含的图片名称,通过相关方法即可从assert加载对应图片,详情请看本文的示例程序。
public void addImage() {
List<PhotoAnnotation> demoObjList = new ArrayList<>();
Random random = new Random();
// 随机添加100张图片(在中国范围内)
for (int i = 0; i < 100; i++) {
int lat = random.nextInt(30) + 20;
int lng = random.nextInt(64) + 71;
String imageName = random.nextInt(9) + ".jpg";
// Log.d(TAG,"imageName: " + imageName);
PhotoAnnotation demoObj = new PhotoAnnotation("" + i,
imageName, lat, lng);
demoObjList.add(demoObj);
}
mapBoxUtil.setClusterLayerPhotoItem(demoObjList);
// 缩小地图
mapBoxUtil.moveCameraTo(Point.fromLngLat(103, 35), 2, 1000);
}
最后将 mapGeoJson 传入属性data中即可(注意,这一步需要在主线程中进行,关于mapView的操作均需要在主线程中进行,因为都是涉及UI的更改):
/**
* 向聚类源中设置数据集
* @param mapGeoJson 为空时则清空数据
*/
private void setClusterSourceProperties(MapGeoJson mapGeoJson) {
if (mStyle == null) {
return;
}
JSONObject properties = new JSONObject();
try {
properties.put("type","geojson");
if (mapGeoJson == null) {
properties.put("data","");
}else {
properties.put("data",mapGeoJson.crateJson());
}
} catch (JSONException e) {
e.printStackTrace();
}
Expected<String, Value> out = Value.fromJson(properties.toString());
if (out.getValue() == null) {
Log.d(TAG,"[setClusterSourceProperties]property turn to value error:" + out.getError());
return;
}
Expected<String, None> success = mStyle.setStyleSourceProperties(CLUSTER_SOURCE_ID, out.getValue());
Log.d(TAG,"[setClusterSourceProperties]" + (success.isError() ? success.getError() : "success"));
}
最后还有一步,即
5. 向 style 中添加图片,以用于展示:
利用 style 的 addImage 方法,该方法接受两个参数:
- 一个为图像标识id;
- 一个为图像bitmap。
在 layer 中标记物显示时,会根据我们上面创建的 layer 中 icon-image 属性的值,在 style 中检索对应的 图像标识id 进行显示。
mStyle.addImage();
// 注意,在不使用时记得清理image:
mStyle.removeStyleImage(imageId)
为了实现自定义标记物的展示,我们可以构建一个自定义layout,加载成view,再转成bitmap传入即可(记得转化前需要先进行layout操作以赋予view大小)。
layout代码:
/**
* 设置view的大小
*/
public static void layoutView(View v, int width, int height) {
if (v == null || width <= 0 || height <=0) {
return;
}
v.layout(0,0, width, height);
int measuredWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST);
int measuredHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
v.measure(measuredWidth, measuredHeight);
v.layout(0,0, v.getMeasuredWidth(), v.getMeasuredHeight());
}
view转bitmap代码:
/**
* 从view中创建bitmap
* @param view 需要有大小
*/
public static Bitmap getBitmapFromView(View view) {
if (view == null || view.getWidth() <= 0 || view.getHeight() <= 0){
return null;
}
//是ImageView直接获取
if (view instanceof ImageView) {
Drawable drawable = ((ImageView) view).getDrawable();
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
}
view.clearFocus();
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
if (bitmap != null) {
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
canvas.setBitmap(null);
}
return bitmap;
}
6. 点击标记物的实现(若为聚合对象,可以获取到其中的包含的所有标记物):
通过 mMapboxMap 的 queryRenderedFeatures 方法,我们可以根据用户点击相对于 mapView 左上角的坐标,查询到指定Layers中的标记对象。
若标记对象为聚合对象,我们再利用 getGeoJsonClusterLeaves 方法,根据聚合对象的特征获取到该聚合对象中包含的所有标记物对象。
相关代码如下所示:
/**
* 地图点击事件响应(用于判断被点击组件是否为cluster)
* @param clickX 触摸点相对于mapView左上角的坐标X
* @param clickY 触摸点相对于mapView左上角的坐标Y
*/
public void onMapClickEvent(float clickX, float clickY) {
if (mMapboxMap == nulll) {
return;
}
// Log.d(TAG,"onMapClickEvent(" + clickX + "," + clickY + ")");
// 单张图片点击事件
mMapboxMap.queryRenderedFeatures(
new RenderedQueryGeometry(new ScreenCoordinate(clickX, clickY)),
new RenderedQueryOptions(Collections.singletonList(LAYER_ID_SINGLE_POINT), null),
features -> {
if (features.getError() != null || features.getValue() == null) {
return;
}
List<QueriedFeature> list = features.getValue();
if (list == null || list.size() <= 0) {
return;
}
// 默认取第一个
QueriedFeature curFeature = list.get(0);
JsonObject jsonObject = curFeature.getFeature().properties();
if (jsonObject == null) {
return;
}
Log.d(TAG,"[getClusterChildFromClick]"+jsonObject.toString());
String id = jsonObject.get("id").getAsString();
//id 即为被点击的标记物id
});
// 聚合点点击事件
mMapboxMap.queryRenderedFeatures(
new RenderedQueryGeometry(new ScreenCoordinate(clickX, clickY)),
new RenderedQueryOptions(Arrays.asList(LAYER_ID_CLUSTER_ + "0",
LAYER_ID_CLUSTER_ + "1",
LAYER_ID_CLUSTER_ + "2"), null),
features -> {
if (features.getError() != null) {
return;
}
List<QueriedFeature> list = features.getValue();
if (list == null || list.size() <= 0) {
return;
}
// 默认取第一个
QueriedFeature curFeature = list.get(0);
Log.d(TAG,"[getClusterChildFromClick]"+curFeature.toString());
// 查询该聚合下的子集
long maxPointNum = 5000; // 单次查询返回子集数量最大值
mMapboxMap.getGeoJsonClusterLeaves(CLUSTER_SOURCE_ID, curFeature.getFeature(),
maxPointNum, extension -> {
if (extension.getError() != null || extension.getValue() == null) {
return;
}
FeatureExtensionValue features1 = extension.getValue();
if (features1 == null || features1.getFeatureCollection() == null) {
return;
}
Log.d(TAG,"include features size:"+features.getFeatureCollection().size());
List<String> idList = new ArrayList<>();
for (Feature feature : features1.getFeatureCollection()) {
String id = feature.getProperty("id").getAsString();
idList.add(id);
}
//idList即为被点击的聚合对象包含的所有标记物id
});
});
}
关于如何获取到用户点击相对于mapview左上角坐标:
mapView 的 onClick 方法不会返回点击坐标信息。我们应该使用 onTouch 方法,通过返回的event.getX() 和 event.getY() 来获取用户点击相对于mapview左上角坐标。但注意要记得区分单击和双击事件,可以利用 GestureDetector 进行区分。
GestureDetector mGestureDetector = new GestureDetector(... ...);
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
// mapView单击
return false;
}
... ...
}
mapView.setOnTouchListener((v, event) -> mGestureDetector.onTouchEvent(event));