[Huawei Developers] nurshat:
Introduction

Hello reader, Since Android supported system wide dark mode since Android 10, it became important that developers support dark mode in their apps to provide good user experience. That is why in this article I am going to explain how to achieve automatic dark mode with HMS Map Kit.

If you have never integrate HMS Map Kit in your app, please refer to this site.

Once we are finished with the steps stated in the link above, we can move on to Android Studio.

Permissions

Map Kit requires internet connection to function, so let’s add network permissions to manifest file. Read more using link above.

Layout

First, let’s set up our layout xml file. It contains a MapView, and a button in the corner to toggle dark mode manually.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<com.huawei.hms.maps.MapView
android:id="@+id/viewMap"
android:layout_width="match_parent"
android:layout_height="match_parent"
map:cameraTargetLat="48.893478"
map:cameraTargetLng="2.334595"
map:cameraZoom="10" />

<com.google.android.material.button.MaterialButton
android:id="@+id/btnDarkSide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="Dark Side"
map:layout_constraintBottom_toBottomOf="parent"
map:layout_constraintEnd_toEndOf="parent">

</com.google.android.material.button.MaterialButton>
</androidx.constraintlayout.widget.ConstraintLayout>

Logic

First, let’s set up our activity without dark mode functionality. It should look something like this. Don’t forget to set your API key.

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private lateinit var mapView: MapView
private lateinit var huaweiMap: HuaweiMap
private lateinit var darkStyle: MapStyleOptions
private var darkModeOn: Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//TODO a valid api key has to be set
MapsInitializer.setApiKey(Constants.API_KEY)

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

mapView = binding.viewMap
var mapViewBundle: Bundle? = null
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle("MapViewBundleKey")
}
mapView.onCreate(mapViewBundle)
mapView.getMapAsync {
onMapReady(it)
}
}

private fun onMapReady(map: HuaweiMap) {
huaweiMap = map
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)

var mapViewBundle: Bundle? = outState.getBundle("MapViewBundleKey")
if (mapViewBundle == null) {
mapViewBundle = Bundle()
outState.putBundle("MapViewBundleKey", mapViewBundle)
}

mapView.onSaveInstanceState(mapViewBundle)
}

override fun onStart() {
super.onStart()
mapView.onStart()
}

override fun onResume() {
super.onResume()
mapView.onResume()
}

override fun onPause() {
super.onPause()
mapView.onPause()
}

override fun onStop() {
super.onStop()
mapView.onStop()
}

override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}

override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
}

Map Kit supports setting a style through reading a JSON file. You can refer to this site to learn how this JSON file should be stylized.

I’m using the dark style JSON file from official demo. I’ll put the link below. It looks like this, should be located in …\app\src\main\res\raw directory.

[
{
"mapFeature": "all",
"options": "geometry",
"paint": {
"color": "#25292B"
}
},
{
"mapFeature": "all",
"options": "labels.text.stroke",
"paint": {
"color": "#25292B"
}
},
{
"mapFeature": "all",
"options": "labels.icon",
"paint": {
"icon-type": "night"
}
},
{
"mapFeature": "administrative",
"options": "labels.text.fill",
"paint": {
"color": "#E0D5C7"
}
},
{
"mapFeature": "administrative.country",
"options": "geometry",
"paint": {
"color": "#787272"
}
},
{
"mapFeature": "administrative.province",
"options": "geometry",
"paint": {
"color": "#666262"
}
},
{
"mapFeature": "administrative.province",
"options": "labels.text.fill",
"paint": {
"color": "#928C82"
}
},
{
"mapFeature": "administrative.district",
"options": "labels.text.fill",
"paint": {
"color": "#AAA59E"
}
},
{
"mapFeature": "administrative.locality",
"options": "labels.text.fill",
"paint": {
"color": "#928C82"
}
},
{
"mapFeature": "landcover.parkland.natural",
"visibility": false,
"options": "geometry",
"paint": {
"color": "#25292B"
}
},
{
"mapFeature": "landcover.parkland.public-garden",
"options": "geometry",
"paint": {
"color": "#283631"
}
},
{
"mapFeature": "landcover.parkland.human-made",
"visibility": false,
"options": "geometry",
"paint": {
"color": "#25292B"
}
},
{
"mapFeature": "landcover.parkland.public-garden",
"options": "labels.text.fill",
"paint": {
"color": "#8BAA7F"
}
},
{
"mapFeature": "landcover.hospital",
"options": "geometry",
"paint": {
"color": "#382B2B"
}
},
{
"mapFeature": "landcover",
"options": "labels.text.fill",
"paint": {
"color": "#928C82"
}
},
{
"mapFeature": "poi.shopping",
"options": "labels.text.fill",
"paint": {
"color": "#9C8C5F"
}
},
{
"mapFeature": "landcover.human-made.building",
"visibility": false,
"options": "labels.text.fill",
"paint": {
"color": "#000000"
}
},
{
"mapFeature": "poi.tourism",
"options": "labels.text.fill",
"paint": {
"color": "#578C8C"
}
},
{
"mapFeature": "poi.beauty",
"options": "labels.text.fill",
"paint": {
"color": "#9E7885"
}
},
{
"mapFeature": "poi.leisure",
"options": "labels.text.fill",
"paint": {
"color": "#916A91"
}
},
{
"mapFeature": "poi.eating&drinking",
"options": "labels.text.fill",
"paint": {
"color": "#996E50"
}
},
{
"mapFeature": "poi.lodging",
"options": "labels.text.fill",
"paint": {
"color": "#A3678F"
}
},
{
"mapFeature": "poi.health-care",
"options": "labels.text.fill",
"paint": {
"color": "#B07373"
}
},
{
"mapFeature": "poi.public-service",
"options": "labels.text.fill",
"paint": {
"color": "#5F7299"
}
},
{
"mapFeature": "poi.business",
"options": "labels.text.fill",
"paint": {
"color": "#6B6B9D"
}
},
{
"mapFeature": "poi.automotive",
"options": "labels.text.fill",
"paint": {
"color": "#6B6B9D"
}
},
{
"mapFeature": "poi.sports.outdoor",
"options": "labels.text.fill",
"paint": {
"color": "#597A52"
}
},
{
"mapFeature": "poi.sports.other",
"options": "labels.text.fill",
"paint": {
"color": "#3E90AB"
}
},
{
"mapFeature": "poi.natural",
"options": "labels.text.fill",
"paint": {
"color": "#597A52"
}
},
{
"mapFeature": "poi.miscellaneous",
"options": "labels.text.fill",
"paint": {
"color": "#A7ADB0"
}
},
{
"mapFeature": "road.highway",
"options": "labels.text.fill",
"paint": {
"color": "#E3CAA2"
}
},
{
"mapFeature": "road.national",
"options": "labels.text.fill",
"paint": {
"color": "#A7ADB0"
}
},
{
"mapFeature": "road.province",
"options": "labels.text.fill",
"paint": {
"color": "#A7ADB0"
}
},
{
"mapFeature": "road.city-arterial",
"options": "labels.text.fill",
"paint": {
"color": "#808689"
}
},
{
"mapFeature": "road.minor-road",
"options": "labels.text.fill",
"paint": {
"color": "#808689"
}
},
{
"mapFeature": "road.sidewalk",
"options": "labels.text.fill",
"paint": {
"color": "#808689"
}
},
{
"mapFeature": "road.highway.country",
"options": "geometry.fill",
"paint": {
"color": "#8C7248"
}
},
{
"mapFeature": "road.highway.city",
"options": "geometry.fill",
"paint": {
"color": "#706148"
}
},
{
"mapFeature": "road.national",
"options": "geometry.fill",
"paint": {
"color": "#444A4D"
}
},
{
"mapFeature": "road.province",
"options": "geometry.fill",
"paint": {
"color": "#444A4D"
}
},
{
"mapFeature": "road.city-arterial",
"options": "geometry.fill",
"paint": {
"color": "#434B4F"
}
},
{
"mapFeature": "road.minor-road",
"options": "geometry.fill",
"paint": {
"color": "#434B4F"
}
},
{
"mapFeature": "road.sidewalk",
"options": "geometry.fill",
"paint": {
"color": "#434B4F"
}
},
{
"mapFeature": "transit",
"options": "labels.text.fill",
"paint": {
"color": "#4F81B3"
}
},
{
"mapFeature": "transit.railway",
"options": "geometry",
"paint": {
"color": "#5B2E57"
}
},
{
"mapFeature": "transit.ferry-line",
"options": "geometry",
"paint": {
"color": "#364D67"
}
},
{
"mapFeature": "transit.airport",
"options": "geometry",
"paint": {
"color": "#2C3235"
}
},
{
"mapFeature": "water",
"options": "geometry",
"paint": {
"color": "#243850"
}
},
{
"mapFeature": "water",
"options": "labels.text.fill",
"paint": {
"color": "#4C6481"
}
},
{
"mapFeature": "trafficInfo.smooth",
"options": "geometry",
"paint": {
"color": "#348734"
}
},
{
"mapFeature": "trafficInfo.amble",
"options": "geometry",
"paint": {
"color": "#947000"
}
},
{
"mapFeature": "trafficInfo.congestion",
"options": "geometry",
"paint": {
"color": "#A4281E"
}
},
{
"mapFeature": "trafficInfo.extremelycongestion",
"options": "geometry",
"paint": {
"color": "#7A120B"
}
}
]

We’ll read the JSON file like this.

...
//read dark map options from json file from raw folder
darkStyle = MapStyleOptions.loadRawResourceStyle(this, R.raw.mapstyle_night)
...

Then, we have to detect if the system wide dark mode is on using this function. We will set darkModeOn property with this function when activity launches.

...
//detect if system is set to dark mode
private fun isDarkModeOn(): Boolean {

return when (resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) {
Configuration.UI_MODE_NIGHT_YES -> true
Configuration.UI_MODE_NIGHT_NO -> false
Configuration.UI_MODE_NIGHT_UNDEFINED -> false
else -> false
}
}
...

Now we have to set map style onMapReady callback. Pass null value if no custom style is needed, for example when night mode is not active. When the user toggle dark mode on or off, activity will recreate and respective style will be set automatically.

...
private fun onMapReady(map: HuaweiMap) {
huaweiMap = map
if (darkModeOn) huaweiMap.setMapStyle(darkStyle) else huaweiMap.setMapStyle(null)
}
...

Remember the button in the layout file? Let’s set it up to manually switch style.

...
//set on click listener to the button to toggle map style
binding.btnDarkSide.setOnClickListener {
if (darkModeOn) huaweiMap.setMapStyle(null) else huaweiMap.setMapStyle(darkStyle)
darkModeOn = !darkModeOn
}
...

We’re done, we now have a MapView with automatic/manual dark mode switch. Refer to final activity class below. If you want to see how it looks in action, refer to this medium article.

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private lateinit var mapView: MapView
private lateinit var huaweiMap: HuaweiMap
private lateinit var darkStyle: MapStyleOptions
private var darkModeOn: Boolean = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//TODO a valid api key has to be set
MapsInitializer.setApiKey(Constants.API_KEY)

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

//read dark map options from json file from raw folder
darkStyle = MapStyleOptions.loadRawResourceStyle(this, R.raw.mapstyle_night)

//set darkModeOn property using isDarkModeOn function
darkModeOn = isDarkModeOn()

//set on click listener to the button to toggle map style
binding.btnDarkSide.setOnClickListener {
if (darkModeOn) huaweiMap.setMapStyle(null) else huaweiMap.setMapStyle(darkStyle)
darkModeOn = !darkModeOn
}

mapView = binding.viewMap
var mapViewBundle: Bundle? = null
if (savedInstanceState != null) {
mapViewBundle = savedInstanceState.getBundle("MapViewBundleKey")
}
mapView.onCreate(mapViewBundle)
mapView.getMapAsync {
onMapReady(it)
}
}

//detect if system is set to dark mode
private fun isDarkModeOn(): Boolean {

return when (resources.configuration.uiMode.and(Configuration.UI_MODE_NIGHT_MASK)) {
Configuration.UI_MODE_NIGHT_YES -> true
Configuration.UI_MODE_NIGHT_NO -> false
Configuration.UI_MODE_NIGHT_UNDEFINED -> false
else -> false
}
}

private fun onMapReady(map: HuaweiMap) {
huaweiMap = map
if (darkModeOn) huaweiMap.setMapStyle(darkStyle) else huaweiMap.setMapStyle(null)
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)

var mapViewBundle: Bundle? = outState.getBundle("MapViewBundleKey")
if (mapViewBundle == null) {
mapViewBundle = Bundle()
outState.putBundle("MapViewBundleKey", mapViewBundle)
}

mapView.onSaveInstanceState(mapViewBundle)
}

override fun onStart() {
super.onStart()
mapView.onStart()
}

override fun onResume() {
super.onResume()
mapView.onResume()
}

override fun onPause() {
super.onPause()
mapView.onPause()
}

override fun onStop() {
super.onStop()
mapView.onStop()
}

override fun onDestroy() {
super.onDestroy()
mapView.onDestroy()
}

override fun onLowMemory() {
super.onLowMemory()
mapView.onLowMemory()
}
}

Conclusions

By modifying the HMS Map Kit style JSON file, we can achieve map views with custom styles. We can even switch out styles on runtime for our needs. Thank you.

Reference

Map Kit Demo