The following is my series of related articles, you can refer to, you can give a like or follow my article.

[Android] How to make a chapter list of apps with a crash rate of less than three per thousand

Today’s introduction is the necessary module of large APP – map module. Currently, the largest map SDK in the world should be Google Map. However, Google Map cannot be used in China due to the loss of Google Play Service. However, the most popular map SDK in China are Autonavi and Baidu Map. (If you’re on IOS, there’s a built-in map.)

Recently, the project needs a world map, so we hereby make a compatible module of Amap and Google Maps.

The Sdk access

1. Google Maps, access is relatively easy, of course, because Android itself is Google’s own son. Need to introduce Google service SDK, as well as the Google map SDK https://developers.google.com/places/android-api/start, need gmail account for as the management

2. Amap access is relatively complicated, so you can choose 2D, 3D, positioning and search modules to access the map. Then you need to apply for an account, such as an email address and a mobile phone number. You can use the keyTools command to present the sha1 value of keystore. The package name and sha1 value are bound to each other, and each request will be verified. Then configure meta-data in AndroidManifest.

Preview the module

1. Scott map is through the SDK provides com amap. API. Maps2d. MapView custom map View. Google Maps provides a Fragment space through the SDK to achieve map acquisition

<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
Copy the code

2. Map preview

Google Map and Amap interface related names are similar, the more commonly used interface moveCamera window transfer zoom level is divided into 1~17, the greater the value of the map more accurate addMarker add map label

Google Maps uses getMapAysnc and has onMapReady interface callbacks

mapFragment? .getmapAsync (this) /** * Map ready */ Override fun onMapReady(googleMap: googleMap?) { googleMap ? :returnWith (forward) Google {val latLng = latLng (latitude and longitude) / / visual transfer moveCamera (CameraUpdateFactory. NewLatLngZoom (latLng, 16f)) // Add coordinate addMarker(MarkerOptions().position(latLng))}}Copy the code

Amap uses the setOnMapLoadedListener method to set up callbacks

aMap? .setonmaploadedListener {// Visual movement aMap? MoveCamera (CameraUpdateFactory. NewLatLngZoom (LatLng (latitude and longitude), 100 f)) / / add coordinates aMap? AddMarker (MarkerOptions (). The anchor (0.5 f, 0.5 f.) icon (BitmapDescriptorFactory fromBitmap (BitmapFactory. DecodeResource (resources, R.drawable.common_drag_location))) .position(LatLng(latitude, longitude))) }Copy the code

What if you don’t want the map’s coordinates and visual points to be centered? If you want to move up, you need to set the marginTop to negative, so that the center of the map will move, and the visual point will move up as well.

Positioning module

1. Autonavi provides AMapLocationListener as a listener specifically designed to provide Autonavi coordinates

      private fun setUpMap() { myLocationStyle = MyLocationStyle() myLocationStyle? .strokecolor (color.argb (0, 0, 0, 0))// Set the circular border Color myLocationStyle? .radiusfillColor (color.argb (0, 0, 0, 0))// Set the fill Color of the circle myLocationStyle? Mon_self_location myLocationIcon (BitmapDescriptorFactory. FromResource (R.drawable.com)) / / show its location coordinates aMap?.setMyLocationStyle(myLocationStyle) aMap?.isMyLocationEnabled =true/ / set totrueIndicates that the positioning layer is displayed and positioning can be triggered,falseIndicates that the location layer is hidden and location cannot be triggered. The default isfalseval uriSettings = aMap? .uiSettings uriSettings? .isZoomControlsEnabled =false// Turn off the zoom key} private funinitLoc() {// Initialize mLocationClient = AMapLocationClient(context!! .applicationContext) // Set the location callback to listen on mLocationClient? .setLocationListener(this) // Initializes the location parameter mLocationOption = AMapLocationClientOption() // Sets the location mode to high-precision, Battery_Saving to low-power, Device_Sensors is device-only mode mLocationOption? . LocationMode = AMapLocationClientOption. AMapLocationMode. Whether Hight_Accuracy / / set the return address information (the default return address information) mLocationOption? .isNeedAddress =true// Sets whether to locate only once. The default value isfalsemLocationOption? .isOnceLocation =false// Set whether to force refresh WIFI, default is force refresh mLocationOption? .isWifiActiveScan =false// Sets whether to allow simulation positions. The default isfalse, do not allow simulated positions mLocationOption? .isMockEnable =false// Set the location interval in milliseconds. The default is 3000ms mLocationOption? .interval = (3000) // Set the location parameter for the location client object mLocationClient?.setLocationOption(mLocationOption) // start the location MLocationClient?.startLocation()} Override fun onLocationChanged(amapLocation: amapLocation?) {Copy the code

Google uses the Android native Location 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) /** * map ready */ 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?) {}Copy the code

Search module

1. There are two ways of Google search. One is to search out relevant nearby places through WebAPI (RxVolley’s framework is used here), which has more benefits of association results. The concatenation of uri. Builder is not used here because it specifies that utF-8 format conversions will have “, “strongly converted to” % “signs

    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)
    }
Copy the code

The second type is text linked search (location autocomplete). Google provides a custom Fragment, but if you have advanced customization and don’t use the AutoCompleteTextView, you need to define an Adapter to retrieve the content. (Search results are relatively few.) On this side, the search needs advanced definition, so the form of Adapter is used.

    override fun doSearch(key: String, city: String?) { Observable.create(ObservableOnSubscribe<ArrayList<AutocompletePrediction>> { it.onNext(getAutocomplete(key)!!) / / need in time thread}). SubscribeOn (Schedulers. IO ()). ObserveOn (AndroidSchedulers. MainThread ()). The subscribe ({searchAdpater? .clearData()for (item init) { val placeResult = mGeoDataClient!! GetPlaceById (item. PlaceId) placeResult. AddOnCompleteListener (mUpdatePlaceDetailsCallback) / / asynchronous access detailed information a single placeId}}, {ALog. E (TAG, it)})} / * * * asynchronous access individual placeId details * / 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 andwait for at most
        // 60s forA result from the API. / / collect text link prediction results 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
        }

    }
Copy the code

2. PoiSearch in Amap supports keyword search and search near latitude and longitude addresses.

/** * latitude and longitude search */ fundoSearchQuery(city: String, latitude: Double, longtitude: Double) {
        query = PoiSearch.Query(""."", city) // The first argument represents the search string, the second argument represents the POI search type, and the third argument represents the POI search region (empty string represents the country) query? .pagesize = 20 // Set the maximum number of PoiItem queries returned per page. Val poiSearch = poiSearch (context, Query) poiSearch. SetOnPoiSearchListener (onPoiSearchListener) / / set the search area for the lp point as the center of the circle, Poisearch. bound = poisearch. SearchBound(LatLonPoint(latitude, Longtitude), 1000,true/** * Keyword search */ fundoSearchQuery(keyWord: String, city: String?) {
        if(city ! = null) query = PoiSearch.Query(keyWord,"", city)
        else
            query = PoiSearch.Query(keyWord, ""."") query? .pagesize = 20 // Set the maximum number of PoiItem queries returned per page. Val poiSearch = poiSearch (context!! , query) poiSearch. SetOnPoiSearchListener (onSearchListener) poiSearch. SearchPOIAsyn () / / asynchronous search}Copy the code

Asynchronous returns are also supported

/ * * / * * search results 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) {//Copy the code

Map thumbnail capture

1. Both Amap and Google Maps require web APIS to get thumbnails

        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()
Copy the code

The thing to notice here

1. Autonavi size splicing uses *, while Google size splicing uses “X”. 2. Google needs placeKey, not MapKey. 3. The thumbnails of both maps do not support custom marker 4. Scott abroad cannot show the Google map address details [Scott thumbnail shows] (http://lbs.amap.com/api/webservice/guide/api/staticmaps/), Google thumbnails

1. Amap uses Autonavi coordinates, not standard GPS coordinates. While Autonavi only provided other maps and GPS to Autonavi coordinates, did not provide Autonavi to other coordinates. The standard GPS coordinates used by Google, so if you need to coordinate intercommunication needs to be converted to each other, here provides the way of coordinate conversion to each other.

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()
        }

        returnNull} /** * CONVERT GPS coordinates to Gaode */ 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// PI GCJ_02_To_WGS_84 var PI = 3.14159265358979324 /** * It can be used to solve the conversion problem of latitude and longitude obtained by Android system using Autonavi SDK. * @param The latitude and longitude to convert * @return{@inheritDoc} exception description </ exception type > */ Fun delta(lat: Double, lon: Double): HashMap<String, Double> {val a = 6378245.0// Val ee = 0.00669342162296594323// Val ee = 0.00669342162296594323// first eccentricity square var dLat = This. TransformLat (LON-105.0, LAT-35.0) var dLon = this. TransformLon (Lon-105.0, LAT-35.0) 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)

        returnFun 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.0returnRet} // Convert latitude 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.0return ret
    }
}
Copy the code

1. If autonavi wants to display foreign maps, it can choose to use Google tiles

/** * Load online tile data */ private funuseOMCMap() {
//        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 satellite map // val url ="http://mt0.google.cn/vt/lyrs=y@198&hl=zh-CN&gl=cn&src=app&x=%d&y=%d&z=%d&s="// satellite map 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"// Plane mapif (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))// In order not to display in the pictures of the mobile phone, the downloaded pictures should cancel the JPG suffix, and the file name should be defined by myself, and the file name can be written and read in the same way. Because of the own bingmap source service, so HERE I use the filename of bingmap val LJ = fileapi.map_directory + mFileDirName + mFileNameif(MapImageCache. Instance. IsBitmapExit (mFileDirName + mFileName)) {/ / whether the local image files, if there is a return to the local url, if not, to the local cache and returns googleurlreturn URL("file://" + LJ)
                        } else{ val filePath = String.format(url, x, y, zoom) val mBitmap: Bitmap //mBitmap = BitmapFactory.decodeStream(getImageStream(filePath)); Val stream = getImageStream(filePath) val stream = getImageStreamif(stream ! = null) { mBitmap = getImageBitmap(stream) try { saveFile(mBitmap, mFileName, mFileDirName) } catch (e: IOException) { e.printStackTrace() } }return URL(filePath)
                        }
                    } catch (e: Exception) {
                        e.printStackTrace()
                    }

                    returnnull } }) tileOverlayOptions!! .diskCacheEnabled(falseDiskCacheDir (fileapi.map_directory).diskCachesize (1024000) .memoryCacheEnabled(true).memCachesize (102400).zindex (-9999f)} mtileOverlay = aMap? .addTileOverlay(tileOverlayOptions) mtileOverlay? .isVisible =true
    }
Copy the code

To stop adding tiles in China, remove the tiles

fun stopUseOMCMap() { mtileOverlay? .remove() mtileOverlay? .clearTileCache() mtileOverlay? .isVisible =falseaMap? .removecache() }Copy the code

Note that you don’t want to repeat add Overlay over and over again, because the number of times you call remove needs to match the number of times you call add. Part of the code here has not been pasted, I will open a module demo for everyone to demonstrate, interested students can also contact me in the group.

Special problems

1. Autonavi only supports details of domestic coordinates. If a foreign address is sent abroad to a domestic mobile phone, the domestic mobile phone will use Amap and will not be able to display details of foreign coordinates. 2. Google’s detailed information on domestic coordinates is also relatively limited. 3. Map search involves asynchronous return, please remove the listener onDestroy, otherwise there will be memory leak 4. The mapView of Autonavi will hold the Activity context object and will not release it. When Autonavi searches, if the parameter of city is not added, it will be a national search, and the location of your city may not be found (you can obtain the name of the city when obtaining the location), please handle with caution.

For some more in-depth problems, such as the track of traffic map, the business has not been involved for the time being, if there is a better solution, I will continue to publish a new chapter, thank you.