前言
Osmdroid是Android的MapView (v1 API)类的一个(几乎)完全/免费的替代品。它还包括一个模块化的瓦片提供程序系统,支持许多在线和离线的瓷砖源,并支持内置的覆盖,用于绘制图标、跟踪位置和绘制形状。
本篇主要用来分析Osmdroid加载地图的核心流程,因为只分析核心流程,所以很多细节代码会被省略掉,请注意甄别。
使用
1.build.gradle
implementation 'org.osmdroid:osmdroid-android:6.1.18'
2.布局文件activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<org.osmdroid.views.MapView
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
3.activity
MapView mMapView = findViewById(R.id.map);
mMapView.setTileSource(GoogleTileSource.AutoNaviVector);
IMapController mapController = mMapView.getController();
mapController.setZoom(18.0);
GeoPoint startPoint = new GeoPoint(31.1153958, 121.2994426);
mapController.setCenter(startPoint);
mMapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER);
mMapView.setMultiTouchControls(true);
4.地图source
//高德地图
public static final OnlineTileSourceBase AutoNaviVector = new XYTileSource("AutoNavi-Vector",
0, 20, 256, ".png", new String[]{
"https://wprd01.is.autonavi.com/appmaptile?",
"https://wprd02.is.autonavi.com/appmaptile?",
"https://wprd03.is.autonavi.com/appmaptile?",
"https://wprd04.is.autonavi.com/appmaptile?",
}) {
@Override
public String getTileURLString(long pMapTileIndex) {
return getBaseUrl() + "x=" + MapTileIndex.getX(pMapTileIndex) + "&y=" + MapTileIndex.getY(pMapTileIndex) + "&z="
+ MapTileIndex.getZoom(pMapTileIndex) + "&lang=zh_cn&size=1&scl=1&style=7<ype=7";
}
};
5.效果图
源码分析
1.MapView -> dispatchDraw
绘制所有的overlay,其中getOverlayManager()得到的是DefaultOverlayManager对象。
@Override
protected void dispatchDraw(final Canvas c) {
/* Draw all Overlays. */ 绘制所有overlay
this.getOverlayManager().onDraw(c, this);
}
2.DefaultOverlayManager -> onDraw -> onDrawHelper
@Override
public void onDraw(final Canvas c, final MapView pMapView) {
onDrawHelper(c, pMapView, pMapView.getProjection());
}
mTilesOverlay是TilesOverlay的一个对象
private void onDrawHelper(final Canvas c, final MapView pMapView, final Projection pProjection) {
//always pass false, the shadow parameter will be removed in a later version of osmdroid, this change should result in the on draw being called twice
if (mTilesOverlay != null && mTilesOverlay.isEnabled()) {
if (pMapView != null) {
mTilesOverlay.draw(c, pMapView, false);
} else {
mTilesOverlay.draw(c, pProjection);
}
}
}
3.TilesOverlay -> draw
先设置窗口信息,然后开始绘制。
@Override
public void draw(Canvas c, Projection pProjection) {
//这里是在设置可视窗口
if (!setViewPort(c, pProjection)) {
return;
}
// Draw the tiles! 开始绘制
drawTiles(c, getProjection(), getProjection().getZoomLevel(), mViewPort);
}
TilesOverlay -> setViewPort
getMercatorViewPort 方法用于获取墨卡托投影下的视口信息,视口可以理解为地图在屏幕上显示的可见区域。
protected boolean setViewPort(final Canvas pCanvas, final Projection pProjection) {
setProjection(pProjection);
getProjection().getMercatorViewPort(mViewPort);
return true;
}
4.TilesOverlay -> drawTiles
其中mTileLooper指的是OverlayTileLooper的对象。方法命名为loop,可见其实循环绘制的。
public void drawTiles(final Canvas c, final Projection projection, final double zoomLevel, final RectL viewPort) {
mProjection = projection;
mTileLooper.loop(zoomLevel, viewPort, c);
}
5.OverlayTileLooper -> loop
public void loop(final double pZoomLevel, final RectL pViewPort, final Canvas pCanvas) {
mCanvas = pCanvas;
loop(pZoomLevel, pViewPort);
}
6.TileLooper -> loop(xxx,xxx)
mTiles是一个rect,表示屏幕上的范围,这里通过双层for循环进行加载,可以认为是将屏幕进行切割,屏幕被划分为多个小块,如3x3,4x4这样的结构,然后通过分块请求将每个独立区域的地图图片加载到,再拼接到屏幕上。
protected void loop(final double pZoomLevel, final RectL pMercatorViewPort) {
/* Draw all the MapTiles (from the upper left to the lower right). */
for (int i = mTiles.left; i <= mTiles.right; i++) {
for (int j = mTiles.top; j <= mTiles.bottom; j++) {
if ((horizontalWrapEnabled || (i >= 0 && i < mapTileUpperBound)) && (verticalWrapEnabled
|| (j >= 0 && j < mapTileUpperBound))) {
final int tileX = MyMath.mod(i, mapTileUpperBound);
final int tileY = MyMath.mod(j, mapTileUpperBound);
final long tile = MapTileIndex.getTileIndex(mTileZoomLevel, tileX, tileY);
handleTile(tile, i, j);
}
}
}
}
7.OverlayTileLooper -> handleTile
处理图片的加载和展示。mTileProvider指的就是tile的提供者MapTileProviderBasic,tile提供者存在多级缓存,先从内存中读取,如果读不到再从其他缓存加载,最终是从网络服务器加载。
@Override
public void handleTile(final long pMapTileIndex, int pX, int pY) {
//根据index拿到图片得到图片的drawable
Drawable currentMapTile = mTileProvider.getMapTile(pMapTileIndex);
boolean isReusable = currentMapTile instanceof ReusableBitmapDrawable;
final ReusableBitmapDrawable reusableBitmapDrawable =
isReusable ? (ReusableBitmapDrawable) currentMapTile : null;
//如果drawable为空表示缓存不存在,需要从云端下载,下载过程中页面显示加载中样式
//这个样式通过getLoadingTile来决定。
if (currentMapTile == null) {
currentMapTile = getLoadingTile();
}
//如果drawable不为空,说明存在缓存,调用onTileReadyToDraw绘制到屏幕上
if (currentMapTile != null) {
onTileReadyToDraw(mCanvas, currentMapTile, mTileRect);
}
}
MapTileProviderArray -> getMapTile
先从缓存中读取,如果存在,并判断不过期(代码省略了)就直接返回;若不存在,则进入异步加载流程,返回空。
@Override
public Drawable getMapTile(final long pMapTileIndex) {
final Drawable tile = mTileCache.getMapTile(pMapTileIndex);
if (tile != null) {
return tile;
}
final MapTileRequestState state = new MapTileRequestState(pMapTileIndex, mTileProviderList, MapTileProviderArray.this);
runAsyncNextProvider(state);
return tile;
}
TilesOverlay -> getLoadingTile
加载中的样式什么样的?可以看到下面代码中默认构造了一个和瓦片地图图片大小一样的bitmap,然后给这个bitmap上绘制网格。
private Drawable getLoadingTile() {
final int tileSize = mTileProvider.getTileSource() != null ? mTileProvider
.getTileSource().getTileSizePixels() : 256;
final Bitmap bitmap = Bitmap.createBitmap(tileSize, tileSize,
Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
final Paint paint = new Paint();
canvas.drawColor(mLoadingBackgroundColor);
paint.setColor(mLoadingLineColor);
paint.setStrokeWidth(0);
final int lineSize = tileSize / 16;
for (int a = 0; a < tileSize; a += lineSize) {
canvas.drawLine(0, a, tileSize, a, paint);
canvas.drawLine(a, 0, a, tileSize, paint);
}
mLoadingTile = new BitmapDrawable(bitmap);
return mLoadingTile;
}
TilesOverlay -> onTileReadyToDraw
拿到图片的drawable后是怎么将它渲染到屏幕上的,又是怎么指定渲染的位置的?Drawable方法本身有一个接口setBounds,setBounds方法有四个参数,setBounds(int left, int top, int right, int bottom),这个四参数指的是drawable将在被绘制在canvas的哪个矩形区域内。所以设置好位置之后,调用drawable就会将bitmap绘制到屏幕上了。
protected void onTileReadyToDraw(final Canvas c, final Drawable currentMapTile, final Rect tileRect) {
currentMapTile.setColorFilter(currentColorFilter);
currentMapTile.setBounds(tileRect.left, tileRect.top, tileRect.right, tileRect.bottom);
currentMapTile.draw(c);
}
8.接下来看看Tile的异步加载
final MapTileRequestState state = new MapTileRequestState(pMapTileIndex, mTileProviderList, MapTileProviderArray.this);
runAsyncNextProvider(state);
mTileProviderList中存储的就是加载过程中涉及到的多级缓存,看一张截图:当runAsyncNextProvider方法开始执行后就开始逐个从这些缓存中开始加载。
9.MapTileProviderArray -> runAsyncNextProvider
findNextAppropriateProvider就是遍历mTileProviderList的过程,逐个取出每个provider,并检查有效性后返回,调用loadMapTileAsync方法开始加载。
private void runAsyncNextProvider(final MapTileRequestState pState) {
final MapTileModuleProviderBase nextProvider = findNextAppropriateProvider(pState);
nextProvider.loadMapTileAsync(pState);
}
10.MapTileModuleProviderBase -> loadMapTileAsync
异步线程开始加载。
public void loadMapTileAsync(final MapTileRequestState pState) {
if (mExecutor.isShutdown())
return;
synchronized (mQueueLockObject) {
mPending.put(pState.getMapTile(), pState);
}
mExecutor.execute(getTileLoader());
}
getTileLoader()得到的是TileLoader的子类,它实现了Runnable接口,所以可以通过线程池执行。
@Override
final public void run() {
MapTileRequestState state;
Drawable result = null;
while ((state = nextTile()) != null) {
result = null;
result = loadTileIfReachable(state.getMapTile());
tileLoaded(state, result);
}
}
11.TileLoader -> loadTile
loadTileIfReachable最终调用了loadTile方法,我们直接进入云端数据加载的类MapTileDownloader中的loadTile方法。拿到source,拼接好请求的url,然后调用downloadTile开始请求网络数据。
@Override
public Drawable loadTile(final long pMapTileIndex) throws CantContinueException {
OnlineTileSourceBase tileSource = mTileSource.get();
final String tileURLString = tileSource.getTileURLString(pMapTileIndex);
final Drawable result = downloadTile(pMapTileIndex, 0, tileURLString);
return result;
}
12.TileLoader -> downloadTile
纯粹的网络请求,下载到图片后封装为drawable返回
protected Drawable downloadTile(final long pMapTileIndex, final int redirectCount, final String targetUrl) throws CantContinueException {
//纯粹的网络请求,下载到图片后封装为drawable返回
}
13.TileLoader -> tileLoaded
图片拿到之后,我们重新回到前边的tileLoaded方法,loadTileIfReachable方法执行后拿到drawable,然后调用tileLoaded。
protected void tileLoaded(final MapTileRequestState pState, final Drawable pDrawable) {
pState.getCallback().mapTileRequestCompleted(pState, pDrawable);
}
14.MapTileProviderBase -> mapTileRequestCompleted
拿到图片后第一件事,存入内存缓存,然后通知主线程进行ui刷新。
@Override
public void mapTileRequestCompleted(final MapTileRequestState pState, final Drawable pDrawable) {
//内存缓存
putTileIntoCache(pState.getMapTile(), pDrawable, ExpirableBitmapDrawable.UP_TO_DATE);
// tell our caller we've finished and it should update its view
sendMessage(MAPTILE_SUCCESS_ID);
}
15.SimpleInvalidationHandler
刷新页面,一次请求结束。
@Override
public void handleMessage(final Message msg) {
switch (msg.what) {
case MapTileProviderBase.MAPTILE_SUCCESS_ID:
if (mView != null)
mView.invalidate();
break;
}
}
总结
OsmDroid将地图分为很多层,根据不同的层级进行地图图片的加载,加载地图可以简单的认为是加载多张图片,并最终将图片拼接成完整地图的过程。在OsmDroid地图加载过程中,涉及到多级缓存,通过缓存已加载过的特定层级下的指定区域中的地图图片,从而加速地图图片的显示。