Andorid百度地圖聚合優化(大量marker卡頓)
阿新 • • 發佈:2019-02-15
百度地圖聚合方法使用: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):
其中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減少,節省渲染時間。/** * cluster演算法核心 * @param zoom map的級別 * @return */ @Override public Set<? extends Cluster<T>> getClusters(double zoom) { final int discreteZoom = (int) zoom; final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom); final Set<QuadItem<T>> visitedCandidates = new HashSet<QuadItem<T>>(); final Set<Cluster<T>> results = new HashSet<Cluster<T>>(); final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<QuadItem<T>, Double>(); final Map<QuadItem<T>, com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster<T>> itemToCluster = new HashMap<QuadItem<T>, com.baidu.mapapi.clusterutil.clustering.algo.StaticCluster<T>>(); synchronized (mQuadTree) { for (QuadItem<T> candidate : mItems) { if (visitedCandidates.contains(candidate)) { // Candidate is already part of another cluster. continue; } Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan); Collection<QuadItem<T>> 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<T> cluster = new com.baidu.mapapi.clusterutil.clustering.algo .StaticCluster<T>(candidate.mClusterItem.getPosition()); results.add(cluster); for (QuadItem<T> 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; }
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<MarkerWithPosition> markersToRemove = mMarkers;//需呀刪除的點。請思考什麼樣的點需要被刪除?
final LatLngBounds visibleBounds = mMap.getMapStatus().bound;//地圖在手機螢幕上的可見範圍
//1.新增點
// 找出所有螢幕上的原來的cluster中心點,在增加點的時候有些動畫需要用到這些點
List<Point> existingClustersOnScreen = null;
if (DefaultClusterRenderer.this.mClusters != null && SHOULD_ANIMATE) {
existingClustersOnScreen = new ArrayList<Point>();
for (Cluster<T> 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<MarkerWithPosition> newMarkers = Collections.newSetFromMap(
new ConcurrentHashMap<MarkerWithPosition, Boolean>());//儲存新的clusters中需要顯示的點,轉成MarkerWithPosition型別
for (Cluster<T> 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<Point> newClustersOnScreen = null;
if (SHOULD_ANIMATE) {
newClustersOnScreen = new ArrayList<Point>();
for (Cluster<T> 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<T> 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<Point> mClustersOnScreen,用於儲存在地圖視線內的marker位置,如果視線內的marker沒有發生變化,將不再重新渲染。註釋程式碼:
if (clusters.equals(DefaultClusterRenderer.this.mClusters)) {
mCallback.run();
return;
}
這段程式碼的意思是,如果核心演算法計算後的marker沒有發生變化,那麼就不再執行後面的渲染程式碼,因為移動地圖時聚合marker並沒有發生變化,因此移動地圖時永遠不會出現重繪marker。