因为业务需要,在Android实现Mapbox离线地图下载和实时定位,仿照着官方的例子实现,网上的demo也比较少,
主要代码
如下所示:
public class MainActivity extends MapActicity implements OnLocationClickListener, PermissionsListener, OnCameraTrackingChangedListener {
private static final String TAG = "地图下载";
public static final String JSON_CHARSET = "UTF-8";
public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
private NumberProgressBar progressBar;
private MapboxMap map;
private boolean isEndNotified;
private int regionSelected;
private int count_complete_err=0;
// Offline objects
private OfflineManager offlineManager;
private OfflineRegion offlineRegion;
private Button downloadButton;
private Button listButton;
private LocationComponent locationComponent;
private PermissionsManager permissionsManager;
private boolean isLocating = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mapView = findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
map = mapboxMap;
progressBar = findViewById(R.id.progress_bar);
offlineManager = OfflineManager.getInstance(MainActivity.this);
downloadButton = findViewById(R.id.down);
listButton = findViewById(R.id.list);
downloadButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
downloadRegionDialog();
}
});
listButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
downLoadMap();
}
});
map.setStyle(new Style.Builder().fromUrl(getString(R.string.style_hubei)), new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NonNull Style style) {
// map.getStyle().getLayer("ss").setProperties(PropertyFactory.);
}
});
}
});
}
/**
* 输入离线地图区域名称
*/
private void downloadRegionDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
final EditText regionNameEdit = new EditText(MainActivity.this);
regionNameEdit.setHint(getString(R.string.set_region_name_hint));
builder.setTitle(getString(R.string.dialog_title))
.setView(regionNameEdit)
.setMessage(getString(R.string.dialog_message))
.setPositiveButton(getString(R.string.dialog_positive_button), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String regionName = regionNameEdit.getText().toString();
if (regionName.length() == 0) {
Toast.makeText(MainActivity.this, getString(R.string.dialog_toast), Toast.LENGTH_SHORT).show();
} else {
downloadRegion(regionName);
}
}
})
.setNegativeButton(getString(R.string.dialog_negative_button), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
builder.show();
}
/**
* 下载当前视图范围内地图的离线地图
*
* @param regionName
*/
private void downloadRegion(final String regionName) {
startProgress();
String styleUrl = map.getStyle().getUrl();
//获取当前视图范围,地图下载范围
LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds;
//下载多少级之间的瓦片
double minZoom = map.getCameraPosition().zoom;
// double maxZoom = map.getMaxZoomLevel();
double maxZoom = minZoom + 3;
float pixelRatio = this.getResources().getDisplayMetrics().density;
OfflineTilePyramidRegionDefinition definition = new OfflineTilePyramidRegionDefinition(
styleUrl, bounds, minZoom, maxZoom, pixelRatio);
byte[] metadata;
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put(JSON_FIELD_REGION_NAME, regionName);
String json = jsonObject.toString();
metadata = json.getBytes(JSON_CHARSET);
} catch (Exception exception) {
Timber.e("Failed to encode metadata: %s", exception.getMessage());
metadata = null;
}
offlineManager.createOfflineRegion(definition, metadata, new OfflineManager.CreateOfflineRegionCallback() {
@Override
public void onCreate(OfflineRegion offlineRegion) {
Timber.d("Offline region created: %s", regionName);
MainActivity.this.offlineRegion = offlineRegion;
launchDownload();
}
@Override
public void onError(String error) {
Timber.e("Error: %s", error);
}
});
}
/**
* 监听下载进度
*/
private void launchDownload() {
offlineRegion.setObserver(new OfflineRegion.OfflineRegionObserver() {
@Override
public void onStatusChanged(OfflineRegionStatus status) {
// 计算百分比
double percentage = status.getRequiredResourceCount() >= 0
? (100.0 * (status.getCompletedResourceCount()+count_complete_err )/ status.getRequiredResourceCount()) :
0.0;
if (status.isComplete()||percentage>=100) {
// 下载完
endProgress(getString(R.string.end_progress_success));
return;
} else {
setPercentage((int) Math.round(percentage));
Log.e(TAG, String.valueOf(percentage));
}
}
@Override
public void onError(OfflineRegionError error) {
Log.e(TAG,"下载错误");
Log.e(TAG,error.getMessage());
count_complete_err++;
}
@Override
public void mapboxTileCountLimitExceeded(long limit) {
Timber.e("Mapbox tile count limit exceeded: %s", limit);
}
});
// Change the region state
offlineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE);
}
public void downLoadMap() {
regionSelected = 0;
// 查询离线列表
offlineManager.listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() {
@Override
public void onList(final OfflineRegion[] offlineRegions) {
if (offlineRegions == null || offlineRegions.length == 0) {
Toast.makeText(getApplicationContext(), getString(R.string.toast_no_regions_yet), Toast.LENGTH_SHORT).show();
return;
}
// 将结果添加到列表
ArrayList<String> offlineRegionsNames = new ArrayList<>();
for (OfflineRegion offlineRegion : offlineRegions) {
offlineRegionsNames.add(getRegionName(offlineRegion));
}
final CharSequence[] items = offlineRegionsNames.toArray(new CharSequence[offlineRegionsNames.size()]);
//弹框
AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
.setTitle(getString(R.string.navigate_title))
.setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
regionSelected = which;
}
})
.setPositiveButton(getString(R.string.navigate_positive_button), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
Toast.makeText(MainActivity.this, items[regionSelected], Toast.LENGTH_LONG).show();
//跳转到选中区域
LatLngBounds bounds = ((OfflineTilePyramidRegionDefinition)
offlineRegions[regionSelected].getDefinition()).getBounds();
double regionZoom = ((OfflineTilePyramidRegionDefinition)
offlineRegions[regionSelected].getDefinition()).getMinZoom();
CameraPosition cameraPosition = new CameraPosition.Builder()
.target(bounds.getCenter())
.zoom(regionZoom)
.build();
map.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
})
.setNeutralButton(getString(R.string.navigate_neutral_button_title), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
//删除
progressBar.setProgress(0);
progressBar.setVisibility(View.VISIBLE);
offlineRegions[regionSelected].delete(new OfflineRegion.OfflineRegionDeleteCallback() {
@Override
public void onDelete() {
progressBar.setVisibility(View.INVISIBLE);
progressBar.setProgress(0);
Toast.makeText(getApplicationContext(), getString(R.string.toast_region_deleted),
Toast.LENGTH_LONG).show();
}
@Override
public void onError(String error) {
progressBar.setVisibility(View.INVISIBLE);
progressBar.setProgress(0);
Timber.e("Error: %s", error);
}
});
}
})
.setNegativeButton(getString(R.string.navigate_negative_button_title), null).create();
dialog.show();
}
@Override
public void onError(String error) {
Timber.e("Error: %s", error);
}
});
}
private String getRegionName(OfflineRegion offlineRegion) {
String regionName;
try {
byte[] metadata = offlineRegion.getMetadata();
String json = new String(metadata, JSON_CHARSET);
JSONObject jsonObject = new JSONObject(json);
regionName = jsonObject.getString(JSON_FIELD_REGION_NAME);
} catch (Exception exception) {
Timber.e("Failed to decode metadata: %s", exception.getMessage());
regionName = "未命名";
}
return regionName;
}
// Progress bar methods
private void startProgress() {
count_complete_err=0;
// Disable buttons
downloadButton.setEnabled(false);
listButton.setEnabled(false);
// Start and show the progress bar
isEndNotified = false;
progressBar.setProgress(0);
progressBar.setVisibility(View.VISIBLE);
}
private void setPercentage(final int percentage) {
progressBar.setProgress(0);
progressBar.setProgress(percentage);
}
private void endProgress(final String message) {
// Don't notify more than once
if (isEndNotified) {
return;
}
// Enable buttons
downloadButton.setEnabled(true);
listButton.setEnabled(true);
// Stop and hide the progress bar
isEndNotified = true;
progressBar.setProgress(0);
progressBar.setVisibility(View.GONE);
// Show a toast
Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show();
}
@Override
public void onExplanationNeeded(List<String> permissionsToExplain) {
Toast.makeText(this, "需要定位权限", Toast.LENGTH_LONG).show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@Override
public void onPermissionResult(boolean granted) {
if (granted) {
map.getStyle(new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NonNull Style style) {
enableLocationComponent(style);
}
});
} else {
Toast.makeText(this, "获取定位权限失败", Toast.LENGTH_LONG).show();
finish();
}
}
@SuppressWarnings({"MissingPermission"})
@Override
public void onLocationComponentClick() {
if (locationComponent.getLastKnownLocation() != null) {
Toast.makeText(this,
"纬度" + locationComponent.getLastKnownLocation().getLatitude() + "经度" +
locationComponent.getLastKnownLocation().getLongitude(), Toast.LENGTH_LONG).show();
}
}
@SuppressWarnings({"MissingPermission"})
private void enableLocationComponent(@NonNull Style loadedMapStyle) {
// Check if permissions are enabled and if not request
if (PermissionsManager.areLocationPermissionsGranted(this)) {
// Create and customize the LocationComponent's options
LocationComponentOptions customLocationComponentOptions = LocationComponentOptions.builder(this)
.elevation(5)
.accuracyAlpha(.6f)
.accuracyColor(Color.RED)
.foregroundDrawable(android.R.drawable.ic_menu_mylocation)
.build();
// Get an instance of the component
locationComponent = map.getLocationComponent();
LocationComponentActivationOptions locationComponentActivationOptions =
LocationComponentActivationOptions.builder(this, loadedMapStyle)
.locationComponentOptions(customLocationComponentOptions)
.build();
locationComponent.activateLocationComponent(locationComponentActivationOptions);
locationComponent.setLocationComponentEnabled(true);
locationComponent.setCameraMode(CameraMode.TRACKING);
locationComponent.setRenderMode(RenderMode.COMPASS);
locationComponent.addOnLocationClickListener(this);
// Add the camera tracking listener. Fires if the map camera is manually moved.
locationComponent.addOnCameraTrackingChangedListener(this);
} else {
permissionsManager = new PermissionsManager(this);
permissionsManager.requestLocationPermissions(this);
}
}
@Override
public void onCameraTrackingDismissed() {
}
@Override
public void onCameraTrackingChanged(int currentMode) {
}
public void locate(View view) {
Button button = (Button) view;
if (isLocating) {
isLocating = false;
button.setText("定位");
locationComponent.setLocationComponentEnabled(false);
locationComponent.removeOnCameraTrackingChangedListener(this);
locationComponent.removeOnLocationClickListener(this);
} else {
if (map.getStyle() != null) {
button.setText("关闭定位");
enableLocationComponent(map.getStyle());
isLocating = true;
}
}
}
}
效果图
效果如下图所示:
简要说明一下操作,点击开始下载,自动下载当前屏幕区域内的离线地图,根据下载进度,进度条自动计算,下载完成后,点击下载列表,打开离线列表,点击其中的选项,点击确定,会自动跳转到对应的离线地图区域,这时候断开网络连接,也还是继续显示的。点击定位按钮,自动跳转到当前位置。
说明
- Mapbox离线地图下载,会将所有图层均下载,即使是瓦片图层,它也会帮你下载瓦片。
- 下载瓦片失败时,失败的数量是不会计入到已经下载瓦片数量里的,所以下载的进度,计算的时候要把失败数量算上,不然永远不会到100%
- 全部代码地址:https://gitee.com/GISuser/offline