百度地图聚合方法使用:http://blog.csdn.net/aconghui/article/details/50958715;
百度地图聚合源码(上): http://blog.csdn.net/javine/article/details/51195014
百度地图聚合源码(下): http://blog.csdn.net/javine/article/details/51234279
百度地图官方聚合demo,对于大量marker来说,使用起来非常卡,在网上也搜寻的不少资料,但是优化聚合卡的方法基本没找到,这里在研究了聚合源码之后,本人优化的思路,仅供参考(阅读之前,请先浏览聚合源码);
以下是百度地图优化的两个点:
1.降低marker之间聚合的条件。
看下百度地图聚合核心算法(NonHierarchicalDistanceBasedAlgorithm):
/**
* cluster算法核心
* @param zoom map的级别
* @return
*/
@Override
public Set extends Cluster> getClusters(double zoom) {
final int discreteZoom = (int) zoom;
final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom);
final Set> visitedCandidates = new HashSet>();
final Set> results = new HashSet>();
final Map, Double> distanceToCluster = new HashMap, Double>();
final Map, com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster> itemToCluster =
new HashMap, com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster>();
synchronized (mQuadTree) {
for (QuadItem candidate : mItems) {
if (visitedCandidates.contains(candidate)) {
// Candidate is already part of another cluster.
continue;
}
Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);
Collection> clusterItems;
// search 某边界范围内的clusterItems
clusterItems = mQuadTree.search(searchBounds);
if (clusterItems.size() == 1) {
// Only the current marker is in range. Just add the single item to the results.
results.add(candidate);
visitedCandidates.add(candidate);
distanceToCluster.put(candidate, 0d);
continue;
}
com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster cluster =
new com.baidu.mapapi.clusterutil.clustering.algo
.StaticCluster(candidate.mClusterItem.getPosition());
results.add(cluster);
for (QuadItem clusterItem : clusterItems) {
Double existingDistance = distanceToCluster.get(clusterItem);
double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());
if (existingDistance != null) {
// Item already belongs to another cluster. Check if it's closer to this cluster.
if (existingDistance < distance) {
continue;
}
// Move item to the closer cluster.
itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);
}
distanceToCluster.put(clusterItem, distance);
cluster.add(clusterItem.mClusterItem);
itemToCluster.put(clusterItem, cluster);
}
visitedCandidates.addAll(clusterItems);
}
}
return results;
}
其中
final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom),表明marker之间聚合的距离,如果zoomSpecificSpanyu越大越容易聚合,反之越不容易聚合,因此我将它修改为final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom-2)。这样降低了聚合的条件,会使地图上的marker减少,节省渲染时间。
2.减少marker渲染数量(DefaultClusterRenderer)
这是节省时间最大的地方,先看源码:
public void run() {
if (clusters.equals(DefaultClusterRenderer.this.mClusters)) {
mCallback.run();//判断如果新的clusters等于上一次保存的clusters,直接return出去
return;
}
final MarkerModifier markerModifier = new MarkerModifier();//这个类处理显示和动画
final float zoom = mMapZoom;//最新的zoom值
final boolean zoomingIn = zoom > mZoom;//mZoom为上一次保存的zoom值
final float zoomDelta = zoom - mZoom;//zoom变化量级,超过一定量级就不执行动画了
final Set markersToRemove = mMarkers;//需呀删除的点。请思考什么样的点需要被删除?
final LatLngBounds visibleBounds = mMap.getMapStatus().bound;//地图在手机屏幕上的可见范围
//1.添加点
// 找出所有屏幕上的原来的cluster中心点,在增加点的时候有些动画需要用到这些点
List existingClustersOnScreen = null;
if (DefaultClusterRenderer.this.mClusters != null && SHOULD_ANIMATE) {
existingClustersOnScreen = new ArrayList();
for (Cluster c : DefaultClusterRenderer.this.mClusters) { //迭代上一次保存的clusters
if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {//只有已经聚合了的cluster才可以新增点
Point point = mSphericalMercatorProjection.toPoint(c.getPosition());//position转换成point
existingClustersOnScreen.add(point);//保存屏幕上已经聚合的cluster
}
}
}
// Create the new markers and animate them to their new positions.
final Set newMarkers = Collections.newSetFromMap(
new ConcurrentHashMap());//保存新的clusters中需要显示的点,转成MarkerWithPosition类型
for (Cluster c : clusters) { //迭代新的clusters
boolean onScreen = visibleBounds.contains(c.getPosition());//是否在屏幕内
if (zoomingIn && onScreen && SHOULD_ANIMATE) { //地图放大 + 此cluster在屏幕内 + 可以动画(SDK版本>11)
Point point = mSphericalMercatorProjection.toPoint(c.getPosition());//position转成point
Point closest = findClosestCluster(existingClustersOnScreen, point);//找出与这个cluster距离最近的原屏幕上的点
if (closest != null) {//存在,则实现动画
LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);
markerModifier.add(true, new CreateMarkerTask(c, newMarkers, animateTo));
} else {//不存在,则直接添加不生成动画
markerModifier.add(true, new CreateMarkerTask(c, newMarkers, null));
}
} else {//直接添加点,不生成动画
markerModifier.add(onScreen, new CreateMarkerTask(c, newMarkers, null));
}
}
// 2.等待添加点的任务完成
markerModifier.waitUntilFree();
// 把newMarkers中的点从markersToRemove中移除,markersToRemove中的点都是需要从地图上移除的
markersToRemove.removeAll(newMarkers);
//3.移除点
// 找出现在屏幕上显示的cluster中心点,在移除点时需要用到这些点来实现动画
List newClustersOnScreen = null;
if (SHOULD_ANIMATE) {
newClustersOnScreen = new ArrayList();
for (Cluster c : clusters) {
if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {
Point p = mSphericalMercatorProjection.toPoint(c.getPosition());
newClustersOnScreen.add(p);
}
}
}
for (final MarkerWithPosition marker : markersToRemove) { //迭代所有需要移除的点
boolean onScreen = visibleBounds.contains(marker.position);
if (!zoomingIn && zoomDelta > -3 && onScreen && SHOULD_ANIMATE) { // 地图缩小 + zoom改变不超过3
final Point point = mSphericalMercatorProjection.toPoint(marker.position);
final Point closest = findClosestCluster(newClustersOnScreen, point);//找出最近的cluster
if (closest != null) {
LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);//动画移动的终点
markerModifier.animateThenRemove(marker, marker.position, animateTo);
} else {
markerModifier.remove(true, marker.marker);//无动画
}
} else {
markerModifier.remove(onScreen, marker.marker);//无动画
}
}
//等待移除点的任务完成
markerModifier.waitUntilFree();
mMarkers = newMarkers;//保存新的点
DefaultClusterRenderer.this.mClusters = clusters;
mZoom = zoom;//保存最新的zoom
mCallback.run();//执行线程执行完成的回调函数
}
这是渲染marker的代码部分,我们看到在这部分代码中,说明渲染时是先添加点位,再删除点位,主要耗时部分在于添加点位,百度的做法是将所有的merker全部添加一遍,这样非常消耗时间,特别在大量merker的时候。我的思路是,只渲染屏幕能看到的marker:
for (Cluster c : clusters) {
boolean onScreen = visibleBounds.contains(c.getPosition());
if(onScreen){
if (zoomingIn && SHOULD_ANIMATE) {
Point point = mSphericalMercatorProjection.toPoint(c.getPosition());
Point closest = findClosestCluster(existingClustersOnScreen, point);
if (closest != null) {
LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest);
markerModifier.add(true, new CreateMarkerTask(c, newMarkers, animateTo));
} else {
markerModifier.add(true, new CreateMarkerTask(c, newMarkers, null));
}
} else {
markerModifier.add(onScreen, new CreateMarkerTask(c, newMarkers, null));
}
}
}
只有在视线范围内的merker才去创建CreateMarkerTask渲染,不在视线范围内的不创建CreateMarkerTask,好了性能方面优化接结束了,不敢说能承载多少marker量,我想1W以内应该还能接受吧。
补充:
经过上述2优化后,会出现一个问题,就是移动地图时不会重新渲染marker,只有缩放地图时才重新渲染merker.
解决方案:
找到ClusterManager的onMapStatusChange方法,注释一下代码:
if (mPreviousCameraPosition != null && mPreviousCameraPosition.zoom == position.zoom) {
return;
}
这段代码
的意思是地图zoom不发生变化时,将不调用后面的方法。
找到2中的源码,在优化后的这个循环之前添加:
if(DefaultClusterRenderer.this.mClustersOnScreen!=null&&DefaultClusterRenderer.this.mClustersOnScreen.equals(existingClustersOnScreen)){
mCallback.run();
return;
}
DefaultClusterRenderer.this.mClustersOnScreen = existingClustersOnScreen;
并在DefaultClusterRenderer中添加新属性List注释代码:
if (clusters.equals(DefaultClusterRenderer.this.mClusters)) {
mCallback.run();
return;
}
这段代码的意思是,如果核心算法计算后的marker没有发生变化,那么就不再执行后面的渲染代码,因为移动地图时聚合marker并没有发生变化,因此移动地图时永远不会出现重绘marker。