This article is about using Google Map for developing Android native application. Does not cover API pricing, or marketing.
This article does not cover how to set up Google Map project for Android from the scratch either. If you need it, please refer here.
I was building an Android native application by using Google Map SDK. During the development time, I was confused by so many different code examples and API documentation. Now I am trying to write them all down for memorizing and also for sharing some thoughts.
MapView or MapFragment or SupportMapFragment
If you are thinking between MapFragment and SupportMapFragment, choose SupportMapFragment.
Here is my WHY journey:
The simple answer is SupportMapFragment, which supports all Android API Levels.
While it is not that simple. Even there is a lot of popular code examples showing how to use Google Map by using SupportMapFragment, but according to the official document, seems it is built for support back to lower Android version. And also in the official API document, it gives code example by using MapFragment.
The Maps SDK for Android requires API level 12 or higher for the support of MapFragment objects. If you are targeting an application earlier than API level 12, you can access the same functionality through the SupportMapFragment class. You must also include the Android Support Library.
So seems like if our apps are targeting Android API level 12 and above, we can use MapFragment directly.
So MapFragment is OK?
I thought it is a “Yes”, if we only want to build a map in the fragment, this is the simplest way to start with. We don’t even need to think about the lifecycle thing (I will explain below).
Code in layout would be like this:
<?xml version=”1.0" encoding=”utf-8"?><fragment xmlns:android=”http://schemas.android.com/apk/res/android" android:name=”com.google.android.gms.maps.MapFragment” android:id=”@+id/map” android:layout_width=”match_parent” android:layout_height=”match_parent”/>
Ok, sounds good.
But WAIT!
I had a peek at MapFragment source code, found out… it depends on android.app.Fragment, and android.app.Fragment is deprecated in Android API level 28.
This class was deprecated in API level 28.
Use the Support Library Fragment for consistent behavior across all devices and access to Lifecycle.
From here
It leads to the result that we should use SupportMapFragment instead.
Now my layout was updated to:
<?xml version=”1.0" encoding=”utf-8"?><fragment xmlns:android=”http://schemas.android.com/apk/res/android" android:name=”com.google.android.gms.maps.SupportMapFragment” android:id=”@+id/map” android:layout_width=”match_parent” android:layout_height=”match_parent”/>
What about MapView?
Use MapView, when you want to build a Fragment with other functionalities combining with Map.
Since I have an app provides search functionality on the map, so we need to… not only show the map itself, but also some other filter options. They are all in one Fragment, so I choose MapView instead.
With MapView, the layout is like:
<androidx.constraintlayout.widget.ConstraintLayout ...… <com.google.android.gms.maps.MapView android:id=”@+id/map” android:layout_width=”match_parent” android:layout_height=”match_parent” android:gravity=”center” app:layout_constraintStart_toStartOf=”parent” app:layout_constraintTop_toTopOf=”parent” />… // other views…
Lite-mode v.s. fully interactive mode
Lite-mode map is only a static bitmap image, which does NOT have user interactions like zoom, pan the map. While you still can click the default toolbar to open the Google Map app, or request directions by using UiSettings.setMapToolbarEnabled(boolean). Or fire the intent by ourselves.
Lifecycle matters!
Lite-mode
When using the MapView class in lite mode, forwarding lifecycle events is optional, except for the following situations:
- It is mandatory to call onCreate(), otherwise, no map will appear.
- If you wish to show the My Location dot on your lite mode map and use the default location source, you will need to call onResume() and onPause(), because the location source will only update between these calls. If you use your own location source, it’s not necessary to call these two methods.
Full interactive mode
When using the API in fully interactive mode, users of the MapView
class must forward all the activity life cycle methods to the corresponding methods in the MapView
class. Examples of life cycle methods include onCreate()
, onDestroy()
, onResume()
, and onPause()...
Note: If we use MapFragment or SupportMapFragment, we don’t need to care lifecycle, which is well managed by themselves.
How to get GeoLocations of the map area on my app screen
If our map view is simple enough as a rectangle, no padding or covered by some fancy icons or views. We can simply get north-east geolocation and south-west geolocation by googleMap.projection.visibleRegion.latLngBounds.
And then we can easily get north-west geolocation by using the north-east latitude and south-west longitude.
northWest = (northEast.latitude, southWest.longitude)
southEast = (southWest.latitude, northEast.longitude)
Then we don’t need to calculate the view x/y on the screen and project to Google map to get the geolocation out.
Place and remove Markers
Place a marker on the map is easy. But after I placed 300 markers on the map, and then want to remove one of them. It is tricky…since we need to find that specific marker object to remove.
markerObject.remove()
My solution is using a HashMap<Geolocation, Maker> to keep the reference. At least we can have an O(1) time complexity to find out that specific marker if we are lucky (when no key collision). Please let me know if you have a better idea.
Customize Markers
If you want to resize the icon or add some other more complicated design in the marker, or even need to dynamically put some text in marker. We could place the design in a View, which will be converted to a Bitmap, and then add to map.
General idea: Design -> View -> Bitmap -> Map Marker
So, first get a layout with your design
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
...>
<ImageView
android:id="@+id/listing_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
...
tools:src="@drawable/map_pin" />
<TextView
android:id="@+id/count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:background="@drawable/shape_round_white"
...
tools:text="12" />
</androidx.constraintlayout.widget.ConstraintLayout>
Second, put the dynamics in your design, and you will need to convert it to a Bitmap:
private fun getCustomisedCountMarker(icon: Int, withCount: Int, withView: View): Bitmap {
withView.findViewById<TextView>(R.id.count)?.let {
it.text = withCount.toString()
}
withView.findViewById<ImageView>(R.id.listing_icon)?.let {
it.background = ContextCompat.getDrawable(it.context, icon)
}
return withView.toBitmap()
}/**
* Convert a view to a bitmap
* refer: https://github.com/googlemaps/android-maps-utils/blob/master/library/src/main/java/com/google/maps/android/ui/IconGenerator.java
*/
fun View.toBitmap(): Bitmap {
val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
measure(measureSpec, measureSpec)
layout(0, 0, measuredWidth, measuredHeight)
val r = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888)
r.eraseColor(Color.TRANSPARENT)
val canvas = Canvas(r)
draw(canvas)
return r
}
And, add this bitmap to your map.
val markerIcon = BitmapDescriptorFactory.fromBitmap(getCustomisedCountMarker(icon, withCount, withView)
val markerOptions = MarkerOptions()
.position(location)
.icon(markerIcon)
// add marker and save marker to map
googleMap.addMarker(markerOptions)
Done.
Map camera moves too fast
I have set a camera move listener (setOnCameraIdleListener) to the Google map, as long as the user moves the map a bit, I got the event and trigger my function, let’s call it lambdaCallback.
But what about the user moves map too fast or keeps move maps here and there, then I can get tons of events during one second. Can we only do things when the user calms down? YES!
A good solution is registering setOnCameraIdleListener inside of setOnCameraMoveListener, e.g.
googleMap.setOnCameraMoveListener { googleMap.setOnCameraIdleListener {
lambdaCallback(cameraPosition)
googleMap.setOnCameraIdleListener(null)
}
}
And call the real function which is lambdaCallback here when map is idle, and unregister it after lambdaCallback is called.
How to build map instance for Dagger2
I would suggest do NOT build Google Map instance in dependency injection (DI).
I am using Dagger2, in my project, I normally build one dagger component for each Fragment, and it’s built just after the Fragment view is created. If we want to build Google Map instance into DI, then we need to wait until get map in onMapReady(). It takes time, depends on the network, sometimes the Android OS.
So considering the responsive time — I don’t want users to wait 1 second or more to see everything else only after we loaded Google Map successfully — suggest take Google Map out the dependency graph, only set it to the view when we get the map instance from onMapReady(). Everything else can start building once Activity/Fragment as usual.
That’s it!
Thanks for reading. Today is 2nd, Oct 2019. I am still learning. Updated at 23rd, Jan. 2020.