细读百度地图点聚合源码(下)---Renderer类解析

Posted Jaivne_Kuang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了细读百度地图点聚合源码(下)---Renderer类解析相关的知识,希望对你有一定的参考价值。

上一篇文章分析了ClusterMananger的整体结构和核心算法 细读百度地图点聚合源码(上),此文是接着上一篇来的。

在本文中,我们将学习如何在UI线程中做大量的操作,并且不会造成界面卡顿。

上次我们讲到ClusterManager类中的cluster()方法,调用ClusterTask后台线程处理核心算法,既然有doInBackground()后台任务函数,就会有onPostExecute()函数来处理后台线程返回的结果,这一篇我们就分析怎么处理返回的结果。

那么我们就从返回的结果开始吧!

private class ClusterTask extends AsyncTask<Float, Void, Set<? extends Cluster<T>>> {
        @Override
        protected Set<? extends Cluster<T>> doInBackground(Float... zoom) {
            mAlgorithmLock.readLock().lock();
            try {
                return mAlgorithm.getClusters(zoom[0]);
            } finally {
                mAlgorithmLock.readLock().unlock();
            }
        }

        @Override
        protected void onPostExecute(Set<? extends Cluster<T>> clusters) {
            mRenderer.onClustersChanged(clusters);
        }
    }

上面就是ClusterTask的源码,后台任务处理算法,然后返回数据给主线程,返回的就是一个Set<? extends Cluster<T>>类型的对象,就是一个包含若干个cluster对象的集合,而cluster对象又是一个包含若干MyItem(implements ClusterItem)的集合。
并且,每个cluster中的那些MyItem都是确定可以聚合成一个点的。
 
那到底要怎么处理这个cluster集合呢?
我们看到是mRenderer来处理的,就是源码中DefaultClusterRenderer类,当然我们也可以继承这个类来实现我们自己的Renderer类,这个等有需求再细说吧。
 
我们还是先来分析DefaultClusterRenderer这个类究竟做了些什么处理。
 
一切都是从onClustersChanged(clusters)这个方法开始(该方法从ClusterRenderer接口实现而来)。
@Override
    public void onClustersChanged(Set<? extends Cluster<T>> clusters) {
        mViewModifier.queue(clusters);
    }
这个方法很简单,里面只有一句代码,所以我们还得往里面跟踪。
至此我们会有两个疑问:mViewModifier是什么东东?queue()函数又是做什么用的呢?
ViewModifier其实是一个继承于Handler的类,内部只有两个函数:handleMessage()和queue()
我们先来看queue()函数:
public void queue(Set<? extends Cluster<T>> clusters) {
            synchronized (this) {
                // Overwrite any pending cluster tasks - we don't care about intermediate states.
                mNextClusters = new RenderTask(clusters);
            }
            sendEmptyMessage(RUN_TASK);
        }

在函数内部创建一个RenderTask对象,这是一个Runnable接口的实现类,也就是一个未启动的线程。
然后发送一条Message给handleMessage函数。大家应该可以猜到,在handleMessage中肯定会启动这个线程。
下面是handleMessage中的代码:
@Override
        public void handleMessage(Message msg) {
            if (msg.what == TASK_FINISHED) {//线程执行完成
                mViewModificationInProgress = false;//标记线程已经完成
                if (mNextClusters != null) {//如果有新任务还未执行,则再次启动线程
                    // Run the task that was queued up.
                    sendEmptyMessage(RUN_TASK);
                }
                return;
            }
            
            removeMessages(RUN_TASK);

            if (mViewModificationInProgress) {
                // Busy - wait for the callback.
                return;
            }

            if (mNextClusters == null) {
                // Nothing to do.
                return;
            }

            RenderTask renderTask;
            synchronized (this) {
                renderTask = mNextClusters;
                mNextClusters = null;
                mViewModificationInProgress = true;//标记线程正在运行
            }

            renderTask.setCallback(new Runnable() { //设置线程完成时的回调
                @Override
                public void run() {
                    sendEmptyMessage(TASK_FINISHED);//线程完成后,发送消息给自己
                }
            });
            renderTask.setProjection(mMap.getProjection());//无作用
            renderTask.setMapZoom(mMap.getMapStatus().zoom);//设置最新的地图级别
            new Thread(renderTask).start();//启动线程
        }

也不复杂,总的来说就一句话:启动一个线程,这个线程就是RenderTask。
 
程序执行到这里,我们其实只做了一件事情——启动了一个叫做RenderTask的线程。
 
那么,RenderTask究竟是何方神圣呢?
从上面的代码中可以看到,在启动线程之前对RenderTask进行了一些设置,所以在分析它的具体功能前,先看看这些设置有什么作用。
首先是setProjection()方法,此方法在现在的这份源码中没有任何作用。
然后是setMapZoom()方法,看一下源码
public void setMapZoom(float zoom) {
            this.mMapZoom = zoom;
            this.mSphericalMercatorProjection =
                    new SphericalMercatorProjection(256 * Math.pow(2, Math.min(zoom, mZoom)));
        }
可以看到,此方法中会保存当前地图的zoom值,然后创建一个SphericalMercatorProjection对象,它在上一篇核心算法中也出现过,用来实现position(经纬度)和point(二维坐标点)之间的转换。
为了计算点与点之间的距离,我们需要将position转换成point,而为了在地图上绘制marker,我们又需要将point转换成position。
这个转换就公式我就不多分析了,纯数学题。值得一提的是在创建SphericalMercatorProjection对象时传入的参数,其实就是根据zoom计算得到的worldWidth(计算公式:worldWidth = 256 * zoom^2)。至于mZoom,是保存的上一次的zoom值。
其实,zoom值的大小跟最终结果关系不是特别大,唯一的影响就是转换position和point的精度。不知道大家能不能理解,不能理解的话就多看几遍吧。
 
然后,就是RenderTask最重要的run()方法了。
代码逻辑不算特别复杂,还是采用代码+注释的方式来分析吧!
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();//执行线程执行完成的回调函数
        }
 
首先,有两个方法需要说明一下。
shouldRenderAsCluster(cluster) ----- 判断cluster是否能聚合,这一步判断的是cluster中包含的ClusterItem的数量。
/**
     * Determine whether the cluster should be rendered as individual markers or a cluster.
     */
    protected boolean shouldRenderAsCluster(Cluster<T> cluster) {
        return cluster.getSize() > MIN_CLUSTER_SIZE;
    }
findClosestCluster(existingClustersOnScreen, point) ----- 查找最近的cluster。这个查找过程是设定了一个最小距离minDistSquared,如果没有小于minDistSquared的cluster就返回null。
private static Point findClosestCluster(List<Point> markers, Point point) {
        if (markers == null || markers.isEmpty()) {
            return null;
        }
        
        double minDistSquared = MAX_DISTANCE_AT_ZOOM * MAX_DISTANCE_AT_ZOOM;
        Point closest = null;
        for (Point candidate : markers) {
            double dist = distanceSquared(candidate, point);
            if (dist < minDistSquared) {
                closest = candidate;
                minDistSquared = dist;
            }
        }
        return closest;
    }

然后,有个类需要重点介绍----MarkerModifier
MarkerModifier是一个Handler,这也是前篇所说的很精妙的在主线程中做大量工作的方法。
从名称就可以知道,这个是处理地图上Marker的修改。
private MarkerModifier() {
            super(Looper.getMainLooper());
        }
上面是它的构造函数,注意Looper.getMainLooper()
我们知道Handler必须绑定一个Looper对象,而每个线程有且仅有一个Looper对象。但是MarkerModifier绑定的是MainLooper,也就是说它执行的所有内容,全部都在主线程中操作!!!!

此Handler实现了接口MessageQueue.IdleHandler,在读这份源码之前,楼主并不知道这是个什么东东,所以理解得也不一定对,各位最好自己去查一下。
MessageQueue.IdleHandler就是在系统空闲(Idle)状态时调用接口定义的queueIdle()方法,这个方法在实现接口时必须重写。
放在这个项目中,就是在系统空闲的时候,会自动调用MarkerModifier的handleMessage()方法。这样即可以做大量操作,也不会造成系统卡顿。
每次调用MarkerModifier的add方法或者remove方法,都会发送一个Message给自己,以调用自己的handleMessage方法。
接下来,我们着重介绍handleMessage方法。上代码!
        @Override
        public void handleMessage(Message msg) {//把所有的新增和删除marker 以及动画的任务全部执行完成

            if (!mListenerAdded) {
                //添加Idle接口
                Looper.myQueue().addIdleHandler(this); //在主线程空闲时,发送BLANK消息执行点聚合动作
                mListenerAdded = true;
            }
            removeMessages(BLANK);
            lock.lock();
            try {

                // 每次执行10个任务
                // 分批次执行所有任务(增加,删除,动画),避免系统卡顿
                for (int i = 0; i < 10; i++) {
                    performNextTask();//执行一个任务
                }

                if (!isBusy()) {//是否执行完所有任务
                    mListenerAdded = false;
                    //移除idle接口
                    Looper.myQueue().removeIdleHandler(this);//所有子线程全部执行完成
                    // 唤醒所有等待的线程(可以回头去看看RenderTask的run()方法)
                    busyCondition.signalAll();
                } else {
                    //本来这一句是不必要的,但是百度工程师说,某些情况下系统空闲状态不会成功调用queueIdle()方法
                    // 所以这里手动延迟10ms再次调用handleMessage
                    sendEmptyMessageDelayed(BLANK, 10);
                }
            } finally {
                lock.unlock();
            }
        }

到这里,整个源码就分析完了,其他一些方法都比较简单,就不多说了,大家自己看源码吧!!
ClusterDemo

                

以上是关于细读百度地图点聚合源码(下)---Renderer类解析的主要内容,如果未能解决你的问题,请参考以下文章

谷歌地图聚合点

百度地图聚合功能自定义聚合文字

百度地图聚合点加点击事件

教你如何拔取百度地图POI兴趣点

百度地图API 重新生成点聚合的功能

ionic 修改应用名称 及 修改百度离线地图 点聚合 图标