细读百度地图点聚合源码(下)---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
				
时间: 2024-10-20 12:49:34

细读百度地图点聚合源码(下)---Renderer类解析的相关文章

细读百度地图点聚合源码(上)

之前在项目中需要用到百度地图的点聚合,看了百度提供的demo之后,稍微读了一些源码就能达到需求了,所以并未深入解读源码. 最近有空就把百度实现点聚合的源码从里到外仔细研究了一遍受益良多,在此分享一下. 为了方便研究我把百度demo中点聚合相关的类抽出来,新建了个工程,有需要可以下载来研究.ClusterDemo 整个源码分析过程我分为三个部分: 1.整体结构分析 2.核心算法分析 3.实现点聚合 本篇为上篇,主要分析1,2部分.之后还会有个下篇,着重分析具体如何实现marker点聚合以及一些动画

百度网盘采集源码 ,直接采集网盘添加cookies功能

名称:百度网盘采集源码 程序语言:php 数据库:mysql 程序介绍: 1.直接采集百度网盘url 2.前端基于bootstrap 3.搜索考虑到后期上亿数据,是基于coreseek,搜索时间毫秒级. 4.前后端做了非常极致的seo优化 5.资源详情页面 为了使内容聚合度.相关度增加,添加了相关内容 6.精准分词功能 7.热门词自动采集 ps:修修补补将近2个月时间,最终开发完成,在seo方面下了很大功夫,有需要的可以联系 q-q:3420435647 演示网站:http://www.blue

网狐源码下载网狐V5、网狐6603网站后台管理

网狐源码下载网狐V5.网狐6603网站后台管理 冲值系统 2.1 实卡管理: 冲值以生成会员卡类型,附送游戏币的模式.在此模块可进行点卡类型,点卡生成,以及点卡库存明细查询. 概览图: PS:所用源码要为完整源码,本次 所以源码来自网狐源码下载maliwl.com l 类型管理:必须先生成实卡的类型.设置实卡类型的名称,价格,赠付的金币,以及赠送的会员等级,以及此充值卡的用户和服务权限.(注:用户和服务权限慎用.) 问题点:类型里面的赠送金币和会员卡生成的赠送金币是否是重复.这样误导不知道以哪个

3D地图导航应用源码

该源码是一个3D地图导航应用源码,本项目使用了高德地图导航.科大讯飞语音.ShareSDK分享. 可以在地图上选择起点.途经点.终点,然后根据路径规划策略进行路径规划.然后进行地图语音视图导航,并且可以使用ShareSDk分享本地的位置信息. <ignore_js_op> 运行截图 <ignore_js_op> 运行截图 <ignore_js_op> 运行截图 详细说明:http://ios.662p.com/thread-2163-1-1.html

百度地图V2.0实践项目开发工具类bmap.util.js V1.4

/** * 百度地图使用工具类-v2.0(大眾版) * * @author boonya * @date 2013-7-7 * @address Chengdu,Sichuan,China * @email [email protected] * @company KWT.Shenzhen.Inc.com * @notice 有些功能需要加入外部JS库才能使用,另外还需要申请地图JS key . * 申请地址:http://developer.baidu.com/map/apply-key.ht

CountDownLatch &amp; CyclicBarrier源码Android版实现解析

CountDownLatch CountDownLatch允许一条或者多条线程等待直至其它线程完成以系列的操作的辅助同步器. 用一个指定的count值对CountDownLatch进行初始化.await方法会阻塞,直至因为调用countDown方法把当前的count降为0,在这以后,所有的等待线程会被释放,并且在这以后的await调用将会立即返回.这是一个一次性行为--count不能被重置.如果你需要一个可以重置count的版本,考虑使用CyclicBarrier. 其实本类实现非常简单,和Re

用Enterprise Architect从源码自动生成类图

http://blog.csdn.net/zhouyong0/article/details/8281192 /*references:感谢资源分享者.info:简单记录如何通过工具从源码生成类图,便于分析代码结构,对源码阅读挺有用.*/ 看点开源代码学习下,本想找个代码查看方便点的工具,便于理清代码层次,结果发现了Enterprise Architect这一好工具,试用下来还挺方便的.功能上和Rational Rose大致是一类,用处很广,很多我都不懂,知道能画各种UML图,支持的源码语言类型

nginx源码分析--nginx模块解析

nginx的模块非常之多,可以认为所有代码都是以模块的形式组织,这包括核心模块和功能模块,针对不同的应用场合,并非所有的功能模块都要被用到,附录A给出的是默认configure(即简单的http服务器应用)下被连接的模块,这里虽说是模块连接,但nginx不会像apache或lighttpd那样在编译时生成so动态库而在程序执行时再进行动态加载,nginx模块源文件会在生成nginx时就直接被编译到其二进制执行文件中,所以如果要选用不同的功能模块,必须对nginx做重新配置和编译.对于功能模块的选

.NET源码之Page类(二) (转)

.NET源码之Page类(二) 我们在.Net源码之Page类(一) 已经介绍过了初始化与加载阶段了.今天将介绍余下的部分.由于是从源代码上了解生命周期,所以这里会有大量的代码.建议大家看本篇博客的时候最好能够一边对照源代码,最好能够自己调试一遍.希望大家在平时碰到过这方面的问题的,可以留言,能够从源代码这个阶段去剖析问题的实质.         首先我们来回顾一下初始化与加载阶段之间的那个阶段,我们先拿MSDN上对初始化和加载阶段的有2句话描述来看一下: 页初始化阶段:如果当前请求是回发请求,