今天介绍的是大型app必备模块-地图模块。
当今世界最大的地图sdk应该是google地图,但是由于国内墙掉了google play service,国内是无法使用google地图的,然而国内比较热门的地图sdk是高德地图和百度地图。(如果你是IOS,还有自带的地图)
近来项目中需要世界地图,所以特此做了一个高德地图和google地图兼容的模块了。
Sdk接入
1.google地图,接入相对比较简单,当然因为Android本身就是google亲儿子的原因。
需要引入google service的sdk,以及google map的sdk
https://developers.google.com/places/android-api/start,获取账号需要gmail邮箱作为管理
2.高德地图接入相对比较复杂一点,可以选择2d,3d,定位,搜索多种模块去接入地图。
然后需要申请账号,随便邮箱手机号就可以了,通过keytools命令提出keystore的sha1值,包名和
sha1值相互绑定的,每次请求都会验证。
然后配置AndroidManifest中的meta-data。
预览模块
1.高德地图是通过sdk提供的com.amap.api.maps2d.MapView自定义地图View来实现的。
google地图是通过sdk提供一个Fragment空间来实现地图获取
<fragment xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/google_map" android:name="com.google.android.gms.maps.SupportMapFragment" android:layout_width="match_parent" android:layout_height="match_parent"/> mapFragment = childFragmentManager.findFragmentById(R.id.google_map) as SupportMapFragment
2.地图预览
Google地图和高德地图接口相关的名字都是差不多的,比较常用的接口
moveCamera 视窗转移 缩放级别分为1~17级,数值越大地图越精准
addMarker 添加地图标签
google地图是使用getMapAysnc,会有onMapReady的接口回调
mapFragment?.getMapAsync(this) /** * 地图就绪 */ override fun onMapReady(googleMap: GoogleMap?) { googleMap ?: return with(googleMap) { val latLng = LatLng(latitude, longitude) //视觉转移 moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 16f)) //添加坐标 addMarker(MarkerOptions().position(latLng)) } }
高德地图使用setOnMapLoadedListener方法来设置回调
aMap?.setOnMapLoadedListener { //视觉移动 aMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(latitude, longitude), 100f)) //添加坐标 aMap?.addMarker(MarkerOptions().anchor(0.5f, 0.5f) .icon(BitmapDescriptorFactory .fromBitmap(BitmapFactory.decodeResource( resources, R.drawable.common_drag_location))) .position(LatLng(latitude, longitude))) }
如果不想地图的坐标和视觉点显示居中怎么办?
需要将布局中margin上着手,如果想要往上移,就需要将marginTop设置为负值,这样地图中心点就会上移动,并且视觉点也会和中心点一样上移。
定位模块
1.高德提供了AMapLocationListener作为专为提供高德坐标的监听
private fun setUpMap() { myLocationStyle = MyLocationStyle() myLocationStyle?.strokeColor(Color.argb(0, 0, 0, 0))// 设置圆形的边框颜色 myLocationStyle?.radiusFillColor(Color.argb(0, 0, 0, 0))// 设置圆形的填充颜色 myLocationStyle?.myLocationIcon(BitmapDescriptorFactory.fromResource(R.drawable.common_self_location)) //显示自身定位坐标 aMap?.setMyLocationStyle(myLocationStyle) aMap?.isMyLocationEnabled = true// 设置为true表示显示定位层并可触发定位,false表示隐藏定位层并不可触发定位,默认是false val uriSettings = aMap?.uiSettings uriSettings?.isZoomControlsEnabled = false //关掉缩放键 } private fun initLoc() { //初始化定位 mLocationClient = AMapLocationClient(context!!.applicationContext) //设置定位回调监听 mLocationClient?.setLocationListener(this) //初始化定位参数 mLocationOption = AMapLocationClientOption() //设置定位模式为高精度模式,Battery_Saving为低功耗模式,Device_Sensors是仅设备模式 mLocationOption?.locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy //设置是否返回地址信息(默认返回地址信息) mLocationOption?.isNeedAddress = true //设置是否只定位一次,默认为false mLocationOption?.isOnceLocation = false //设置是否强制刷新WIFI,默认为强制刷新 mLocationOption?.isWifiActiveScan = false //设置是否允许模拟位置,默认为false,不允许模拟位置 mLocationOption?.isMockEnable = false //设置定位间隔,单位毫秒,默认为3000ms mLocationOption?.interval = (3000) //给定位客户端对象设置定位参数 mLocationClient?.setLocationOption(mLocationOption) //启动定位 mLocationClient?.startLocation() } override fun onLocationChanged(amapLocation: AMapLocation?) { //监听实时定位 }
google的定位是使用Android原生的Location定位
locationManager = context!!.getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager?.requestLocationUpdates(LocationManager.GPS_PROVIDER, 3000, 10f, this) locationManager?.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 3000, 10f, this) /** *地图就绪 */ override fun onMapReady(googleMap: GoogleMap?) { googleMap ?: return this.googleMap = googleMap with(googleMap.uiSettings) { isZoomGesturesEnabled = true isMyLocationButtonEnabled = true isScrollGesturesEnabled = true } try { googleMap.isMyLocationEnabled = true } catch (e: SecurityException) { ALog.e(TAG, e) } } /** * 定位更新 */ override fun onLocationChanged(location: Location?) { }
搜索模块
1.google 搜索有两种方式,一种是通过webapi来搜索出附近相关的地点(这里使用了RxVolley的框架),这个的好处关联结果比较多。
这里不用Uri.Builder的拼接方式是因为其指定了Utf-8的格式转换将会出现“,”强转为“%”号
val googlePlaceUrl = "https://maps.googleapis.com/maps/api/place/nearbysearch/json" fun getGoogleNearByPlaces(latitude: Double, longitude: Double, radius: Int): Observable<GoogleLocation> { val builder = StringBuilder(googlePlaceUrl) builder.append("?location=").append(latitude.toString()).append(",").append(longitude.toString()) builder.append("&radius=").append(radius.toString()) builder.append("&key=").append(googlePlaceKey) return RxVolley.get<GoogleLocation>(builder.toString(), null, object : TypeToken<GoogleLocation>() {}.type) }
第二种是文字关联搜索(
这边是使用了需要高级定义搜索,所以使用了Adapter的形式。
override fun doSearch(key: String, city: String?) { Observable.create(ObservableOnSubscribe<ArrayList<AutocompletePrediction>> { it.onNext(getAutocomplete(key)!!) //需要在次线程 }).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ searchAdpater?.clearData() for (item in it) { val placeResult = mGeoDataClient!!.getPlaceById(item.placeId) placeResult.addOnCompleteListener(mUpdatePlaceDetailsCallback) //异步访问单个placeId的详细信息 } }, { ALog.e(TAG, it) }) } /** * 异步访问单个placeId的详细信息 */ val mUpdatePlaceDetailsCallback = object : OnCompleteListener<PlaceBufferResponse> { override fun onComplete(task: Task<PlaceBufferResponse>) { try { val place = task.result.get(0) searchAdpater?.addData(LocationItem(false, place.latLng.latitude, place.latLng.longitude, place.name.toString(), place.address.toString())) ALog.i(TAG, "Place details received: " + place.name) task.result.release() } catch (e: RuntimeRemoteException) { ALog.e(TAG, e) } } }private fun getAutocomplete(constraint: CharSequence): ArrayList<AutocompletePrediction>? { ALog.d(TAG, "Starting autocomplete query for: " + constraint) // Submit the query to the autocomplete API and retrieve a PendingResult that will // contain the results when the query completes. val results = mGeoDataClient?.getAutocompletePredictions(constraint.toString(), null, null) // This method should have been called off the main UI thread. Block and wait for at most // 60s for a result from the API. //收集文字关联预测结果 try { Tasks.await<AutocompletePredictionBufferResponse>(results!!, 2, TimeUnit.SECONDS) } catch (e: ExecutionException) { e.printStackTrace() } catch (e: InterruptedException) { e.printStackTrace() } catch (e: TimeoutException) { e.printStackTrace() } try { val autocompletePredictions = results!!.result ALog.d(TAG, "Query completed. Received " + autocompletePredictions.count + " predictions.") // Freeze the results immutable representation that can be stored safely. return DataBufferUtils.freezeAndClose<AutocompletePrediction, AutocompletePrediction>(autocompletePredictions) } catch (e: RuntimeExecutionException) { // If the query did not complete successfully return null Toast.makeText(context, "Error contacting API: " + e.toString(), Toast.LENGTH_SHORT).show() ALog.e(TAG, "Error getting autocomplete prediction API call", e) return null } }
2.高德地图中的PoiSearch是支持通过关键字搜索和经纬度地址附近搜索。
/** * 经纬度搜索 */ fun doSearchQuery(city: String, latitude: Double, longtitude: Double) { query = PoiSearch.Query("", "", city) // 第一个参数表示搜索字符串,第二个参数表示poi搜索类型,第三个参数表示poi搜索区域(空字符串代表全国) query?.pageSize = 20 // 设置每页最多返回多少条poiitem query?.pageNum = 1 // 设置查第一页 val poiSearch = PoiSearch(context, query) poiSearch.setOnPoiSearchListener(onPoiSearchListener) // 设置搜索区域为以lp点为圆心,其周围5000米范围 poiSearch.bound = PoiSearch.SearchBound(LatLonPoint(latitude, longtitude), 1000, true) poiSearch.searchPOIAsyn() // 异步搜索 } /** * 关键字搜索 */ fun doSearchQuery(keyWord: String, city: String?) { if (city != null) query = PoiSearch.Query(keyWord, "", city) else query = PoiSearch.Query(keyWord, "", "") query?.pageSize = 20 // 设置每页最多返回多少条poiitem query?.pageNum = 1 // 设置查第一页 val poiSearch = PoiSearch(context!!, query) poiSearch.setOnPoiSearchListener(onSearchListener) poiSearch.searchPOIAsyn() // 异步搜索 }
当然也支持异步返回
/** * 搜索结果 */ val onSearchListener = object : PoiSearch.OnPoiSearchListener { override fun onPoiSearched(result: PoiResult?, rCode: Int) { if (rCode == 1000) { if (result?.query!! == query) { //返回结果列表 result.pois } } } override fun onPoiItemSearched(p0: PoiItem?, p1: Int) { //返回再搜索 } }
地图缩略图获取
1.高德地图和google地图都需要使用web api来获取缩略图
var builder: StringBuilder? = null if (type == GDMAP) { builder = StringBuilder(gdImgUrl) builder.append("?location=").append(longitude).append(",").append(latitude) builder.append("&zoom=").append(zoom) builder.append("&size=").append(dpToPx(context, width)).append("*").append(dpToPx(context, height)) builder.append("&markers=").append("mid").append(",").append(",A:").append(longitude).append(",").append(latitude) builder.append("&key=").append(gdMapKey) } else if (type == GOOGLEMAP) { builder = StringBuilder(googleImgeUrl) builder.append("?center=").append(latitude).append(",").append(longitude) builder.append("&zoom=").append(zoom) builder.append("&size=").append(dpToPx(context, width)).append("x").append(dpToPx(context, height)) builder.append("&markers=").append(latitude).append(",").append(longitude) builder.append("&key=").append(googlePlaceKey) } return builder.toString()
这里需要注意的是
1.高德size的拼接,是用*号,而google size的拼接是使用“x”。
2.google需要使用的是placeKey,不是mapkey。
3.两种地图的缩略图都不支持自定义标记(marker)
4.高德地图无法显示到国外google的地址的详细信息
[高德缩略图说明](
<a href="https://developers.google.com/maps/documentation/maps-static/intro?hl=zh-cn) [google缩略图说明]
特殊转换
1.高德地图使用的是高德坐标,并不是标准的的GPS坐标。而高德只提供了其他地图和GPS转高德坐标,并不没有提供高德转为其他坐标。Google使用的标准的GPS坐标,那么如果需要坐标互通就需要相互转换了,这里提供了坐标转换相互转换的方式。
object GDConverter { fun fromGpsToLatLng(context: Context, latitude: Double, longitude: Double): LatLng? { val converter = CoordinateConverter(context) converter.from(CoordinateConverter.CoordType.GPS) try { converter.coord(DPoint(latitude, longitude)) val desLatLng = converter.convert() return LatLng(desLatLng.latitude, desLatLng.longitude) } catch (e: Exception) { e.printStackTrace() } return null } /** * GPS坐标转换成高德 */ fun toGDLatLng(latitude: Double, longitude: Double): LatLng { val latLng = LatLng(latitude, longitude) val converter = com.amap.api.maps2d.CoordinateConverter() converter.from(com.amap.api.maps2d.CoordinateConverter.CoordType.GPS) converter.coord(latLng) return converter.convert() } //圆周率 GCJ_02_To_WGS_84 var PI = 3.14159265358979324 /** * 方法描述:方法可以将高德地图SDK获取到的GPS经纬度转换为真实的经纬度,可以用于解决安卓系统使用高德SDK获取经纬度的转换问题。 * @param 需要转换的经纬度 * @return 转换为真实GPS坐标后的经纬度 * @throws <异常类型> {@inheritDoc} 异常描述 </异常类型> */ fun delta(lat: Double, lon: Double): HashMap<String, Double> { val a = 6378245.0//克拉索夫斯基椭球参数长半轴a val ee = 0.00669342162296594323//克拉索夫斯基椭球参数第一偏心率平方 var dLat = this.transformLat(lon - 105.0, lat - 35.0) var dLon = this.transformLon(lon - 105.0, lat - 35.0) val radLat = lat / 180.0 * this.PI var magic = Math.sin(radLat) magic = 1 - ee * magic * magic val sqrtMagic = Math.sqrt(magic) dLat = dLat * 180.0 / (a * (1 - ee) / (magic * sqrtMagic) * this.PI) dLon = dLon * 180.0 / (a / sqrtMagic * Math.cos(radLat) * this.PI) val hm = HashMap<String, Double>() hm.put("lat", lat - dLat) hm.put("lon", lon - dLon) return hm } //转换经度 fun transformLon(x: Double, y: Double): Double { var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)) ret += (20.0 * Math.sin(6.0 * x * this.PI) + 20.0 * Math.sin(2.0 * x * this.PI)) * 2.0 / 3.0 ret += (20.0 * Math.sin(x * this.PI) + 40.0 * Math.sin(x / 3.0 * this.PI)) * 2.0 / 3.0 ret += (150.0 * Math.sin(x / 12.0 * this.PI) + 300.0 * Math.sin(x / 30.0 * this.PI)) * 2.0 / 3.0 return ret } //转换纬度 fun transformLat(x: Double, y: Double): Double { var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)) ret += (20.0 * Math.sin(6.0 * x * this.PI) + 20.0 * Math.sin(2.0 * x * this.PI)) * 2.0 / 3.0 ret += (20.0 * Math.sin(y * this.PI) + 40.0 * Math.sin(y / 3.0 * this.PI)) * 2.0 / 3.0 ret += (160.0 * Math.sin(y / 12.0 * this.PI) + 320 * Math.sin(y * this.PI / 30.0)) * 2.0 / 3.0 return ret } }
高德支持Google瓦片
1.高德地图如果想要显示国外地图,可以选择使用google瓦片
/** * 加载在线瓦片数据 */ private fun useOMCMap() {// val url = "http://www.google.cn/maps/vt?lyrs=y&gl=cn&x=%d&s=&y=%d&z=%d"// val url = "http://mt1.google.cn/vt/lyrs=y&hl=zh-CN&gl=cn&x=%d&s=&y=%d&z=%d" //3D卫星地图// val url = "http://mt0.google.cn/vt/lyrs=y@198&hl=zh-CN&gl=cn&src=app&x=%d&y=%d&z=%d&s=" //卫星地图 val url = "http://mt2.google.cn/vt/lyrs=m@167000000&hl=zh-CN&gl=cn&src=app&x=%d&y=%d&z=%d&s=Galil" //平面地图 if (tileOverlayOptions == null) { tileOverlayOptions = TileOverlayOptions().tileProvider(object : UrlTileProvider(256, 256) { override fun getTileUrl(x: Int, y: Int, zoom: Int): URL? { try { //return new URL(String.format(url, zoom + 1, TileXYToQuadKey(x, y, zoom))); //return new URL(String.format(url, x, y, zoom)); val mFileDirName: String val mFileName: String mFileDirName = String.format("L%02d/", zoom + 1) mFileName = String.format("%s", TileXYToQuadKey(x, y, zoom))//为了不在手机的图片中显示,下载的图片取消jpg后缀,文件名自己定义,写入和读取一致即可,由于有自己的bingmap图源服务,所以此处我用的bingmap的文件名 val LJ = FileApi.MAP_DIRECTORY + mFileDirName + mFileName if (MapImageCache.instance.isBitmapExit(mFileDirName + mFileName)) {//判断本地是否有图片文件,如果有返回本地url,如果没有,缓存到本地并返回googleurl return URL("file://" + LJ) } else { val filePath = String.format(url, x, y, zoom) val mBitmap: Bitmap //mBitmap = BitmapFactory.decodeStream(getImageStream(filePath));//不知什么原因导致有大量的图片存在坏图,所以重写InputStream写到byte数组方法 val stream = getImageStream(filePath) if (stream != null) { mBitmap = getImageBitmap(stream) try { saveFile(mBitmap, mFileName, mFileDirName) } catch (e: IOException) { e.printStackTrace() } } return URL(filePath) } } catch (e: Exception) { e.printStackTrace() } return null } }) tileOverlayOptions!!.diskCacheEnabled(false) //由于高德自带的瓦片缓存在关闭程序后会自动清空,所以无意义,关闭本地缓存 .diskCacheDir(FileApi.MAP_DIRECTORY) .diskCacheSize(1024000) .memoryCacheEnabled(true) .memCacheSize(102400) .zIndex(-9999f) } //增加瓦片贴图 mtileOverlay = aMap?.addTileOverlay(tileOverlayOptions) mtileOverlay?.isVisible = true }
如果需要在中国地域中停止添加瓦片,需要remove掉瓦片
fun stopUseOMCMap() { mtileOverlay?.remove() mtileOverlay?.clearTileCache() mtileOverlay?.isVisible = false aMap?.removecache() }
注意一点,请不要一直触发重复addTileOverlay,调用remove的次数和add的次数是需要对应的。
这里还有一部分的代码没有贴上,我将会开放一个module的demo供大家演示,有兴趣的同学也可以在群内联系我。
特殊问题
1.高德只支持国内坐标详情,如果在国外发送了国外地址到国内手机,国内手机会使用高德地图,将会无法显示国外坐标详情。
2.google对国内坐标信息详情也是比较有限的。
3.地图搜索涉及到异步返回,onDestroy请去掉监听,不然会有内存泄露
4.暂时发现高德的mapview会持有Activity context对象不释放的情况,修复好会在这里更新
5.高德搜索的时候,如果不加入city的参数,将是全国搜索,很可能搜索不到你所在城市的地点(获取定位的时候可以顺带获取所在城市名字),请谨慎处理。
共同学习,写下你的评论
评论加载中...
作者其他优质文章