作者:蒼王 時間:2018.7.1
以下是我這個系列的相關文章,有興趣可以參考一下,可以給個喜歡或者關注我的文章。
[Android]如何做一個崩潰率少於千分之三噶應用app--章節列表
今天介紹的是大型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)
}
複製程式碼
第二種是文字關聯搜尋(地點自動完成),google提供了一個自定義的Fragment,但是如果你有高階定製,不用AutoCompleteTextView,那就需要通過定義一個Adapter來獲取相關內容。(搜尋結果比較少) 這邊是使用了需要高階定義搜尋,所以使用了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的地址的詳細資訊 [高德縮圖說明](http://lbs.amap.com/api/webservice/guide/api/staticmaps/) 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的引數,將是全國搜尋,很可能搜尋不到你所在城市的地點(獲取定位的時候可以順帶獲取所在城市名字),請謹慎處理。
對於一些更加深入的問題,例如交通地圖軌跡這樣的,暫時業務還沒涉及到,如果有更好的解決方案,會繼續上新篇,謝謝。