Android Google Map - Map Type selection from FAB menu

Viewed 2247

I'm trying to create a nice looking menu to change the map type, exactly like what is on Google Maps.. It appears when you click the layers FAB. I don't know if it's a custom FAB menu, or if it animates and opens a fragment.

Screenshot of Google Maps layers menu

How to achieve this look?

2 Answers

After much reading and learning, I have managed to fully design the look that I am happy with. For anyone wanting the same look, here is my code and resources below...

Note: It is not exactly perfect, may look different on some other devices, and don't judge my coding too much (I am a beginner... I can some refactoring but it gets the job done).

Extra Points:

  • Written in Kotlin
  • Pretty sure requires API 21+. Have not implemented any workaround here for earlier versions
  • Open/close of the selection view uses a circular reveal animation, instead of Googles expanding one (I like my implementation better)
  • Lack of optimizations may lead to poor performance on low end devices

Resources


enter image description here type_default.png

enter image description here type_satellite.png

enter image description here type_terrain.png

rounded_rectangle.xml

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#2962ff" />
    <corners android:radius="10dp" />
</shape>

ic_map_layers.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="24dp"
        android:height="24dp"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    <path
        android:fillColor="#3C4043"
        android:pathData="M11.99,18.54l-7.37,-5.73L3,14.07l9,7 9,-7 -1.63,-1.27 
        -7.38,5.74zM12,16l7.36,-5.73L21,9l-9,-7 -9,7 1.63,1.27L12,16z"/>
</vector>

Map Layout File


The code listed here was also nested inside a constraint layout with the map and other FAB, but if you are reading this, I don't think you need to know how to put a map in...

<android.support.design.widget.FloatingActionButton
    android:id="@+id/map_type_FAB"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="24dp"
    android:layout_marginTop="24dp"
    android:clickable="true"
    android:focusable="true"
    app:backgroundTint="#FFF"
    app:fabSize="mini"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="@+id/map_view"
    app:rippleColor="#eff5ff"
    app:srcCompat="@drawable/ic_map_layers" />

<android.support.constraint.ConstraintLayout
    android:id="@+id/map_type_selection"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/map_type_background"
    android:elevation="6dp"
    android:padding="8dp"
    android:visibility="invisible"
    app:layout_constraintEnd_toEndOf="@+id/map_type_FAB"
    app:layout_constraintTop_toTopOf="@+id/map_type_FAB">


    <View
        android:id="@+id/map_type_default_background"
        android:layout_width="54dp"
        android:layout_height="54dp"
        android:background="@drawable/rounded_rectangle"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="@+id/map_type_default"
        app:layout_constraintEnd_toEndOf="@+id/map_type_default"
        app:layout_constraintStart_toStartOf="@+id/map_type_default"
        app:layout_constraintTop_toTopOf="@+id/map_type_default" />

    <ImageButton
        android:id="@+id/map_type_default"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:background="@drawable/type_default"
        android:scaleType="fitCenter"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView13" />

    <View
        android:id="@+id/map_type_satellite_background"
        android:layout_width="54dp"
        android:layout_height="54dp"
        android:background="@drawable/rounded_rectangle"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="@+id/map_type_satellite"
        app:layout_constraintEnd_toEndOf="@+id/map_type_satellite"
        app:layout_constraintStart_toStartOf="@+id/map_type_satellite"
        app:layout_constraintTop_toTopOf="@+id/map_type_satellite" />

    <ImageButton
        android:id="@+id/map_type_satellite"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginEnd="32dp"
        android:layout_marginStart="32dp"
        android:layout_marginTop="8dp"
        android:background="@drawable/type_satellite"
        android:scaleType="fitCenter"
        app:layout_constraintEnd_toStartOf="@+id/map_type_terrain"
        app:layout_constraintStart_toEndOf="@+id/map_type_default"
        app:layout_constraintTop_toBottomOf="@+id/textView13" />

    <View
        android:id="@+id/map_type_terrain_background"
        android:layout_width="54dp"
        android:layout_height="54dp"
        android:background="@drawable/rounded_rectangle"
        android:visibility="invisible"
        app:layout_constraintBottom_toBottomOf="@+id/map_type_terrain"
        app:layout_constraintEnd_toEndOf="@+id/map_type_terrain"
        app:layout_constraintStart_toStartOf="@+id/map_type_terrain"
        app:layout_constraintTop_toTopOf="@+id/map_type_terrain" />

    <ImageButton
        android:id="@+id/map_type_terrain"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginEnd="8dp"
        android:layout_marginTop="8dp"
        android:background="@drawable/type_terrain"
        android:scaleType="fitCenter"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView13" />

    <TextView
        android:id="@+id/textView13"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:fontFamily="sans-serif"
        android:text="Map Type"
        android:textAllCaps="true"
        android:textColor="@android:color/black"
        android:textSize="12sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/map_type_default_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Default"
        android:textColor="#808080"
        android:textSize="12sp"
        app:layout_constraintEnd_toEndOf="@+id/map_type_default"
        app:layout_constraintStart_toStartOf="@+id/map_type_default"
        app:layout_constraintTop_toBottomOf="@+id/map_type_default" />

    <TextView
        android:id="@+id/map_type_satellite_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Satellite"
        android:textColor="#808080"
        android:textSize="12sp"
        app:layout_constraintEnd_toEndOf="@+id/map_type_satellite"
        app:layout_constraintStart_toStartOf="@+id/map_type_satellite"
        app:layout_constraintTop_toBottomOf="@+id/map_type_satellite" />

    <TextView
        android:id="@+id/map_type_terrain_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Terrain"
        android:textColor="#808080"
        android:textSize="12sp"
        app:layout_constraintEnd_toEndOf="@+id/map_type_terrain"
        app:layout_constraintStart_toStartOf="@+id/map_type_terrain"
        app:layout_constraintTop_toBottomOf="@+id/map_type_terrain" />

</android.support.constraint.ConstraintLayout>

Map Fragment


Snippet of code from my onMapReady function. This is the part that isn't the prettiest, but gets the job done.

   override fun onMapReady(googleMap: GoogleMap) {

    // Initialise the map variable
    map = googleMap

    // When map is initially loaded, determine which map type option to 'select'
    when {
        map.mapType == GoogleMap.MAP_TYPE_SATELLITE -> {
            map_type_satellite_background.visibility = View.VISIBLE
            map_type_satellite_text.setTextColor(Color.BLUE)
        }
        map.mapType == GoogleMap.MAP_TYPE_TERRAIN -> {
            map_type_terrain_background.visibility = View.VISIBLE
            map_type_terrain_text.setTextColor(Color.BLUE)
        }
        else -> {
            map_type_default_background.visibility = View.VISIBLE
            map_type_default_text.setTextColor(Color.BLUE)
        }
    }

    // Set click listener on FAB to open the map type selection view
    mapTypeFAB.setOnClickListener {

        // Start animator to reveal the selection view, starting from the FAB itself
        val anim = ViewAnimationUtils.createCircularReveal(
                map_type_selection,
                map_type_selection.width - (map_type_FAB.width / 2),
                map_type_FAB.height / 2,
                map_type_FAB.width / 2f,
                map_type_selection.width.toFloat())
        anim.duration = 200
        anim.interpolator = AccelerateDecelerateInterpolator()

        anim.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationStart(animation: Animator?) {
                super.onAnimationEnd(animation)
                map_type_selection.visibility = View.VISIBLE
            }
        })

        anim.start()
        mapTypeFAB.visibility = View.INVISIBLE

    }

    // Set click listener on the map to close the map type selection view
    map.setOnMapClickListener {

        // Conduct the animation if the FAB is invisible (window open)
        if (map_type_FAB.visibility == View.INVISIBLE) {

            // Start animator close and finish at the FAB position
            val anim = ViewAnimationUtils.createCircularReveal(
                    map_type_selection,
                    map_type_selection.width - (map_type_FAB.width / 2),
                    map_type_FAB.height / 2,
                    map_type_selection.width.toFloat(),
                    map_type_FAB.width / 2f)
            anim.duration = 200
            anim.interpolator = AccelerateDecelerateInterpolator()

            anim.addListener(object : AnimatorListenerAdapter() {
                override fun onAnimationEnd(animation: Animator?) {
                    super.onAnimationEnd(animation)
                    map_type_selection.visibility = View.INVISIBLE
                }
            })

            // Set a delay to reveal the FAB. Looks better than revealing at end of animation
            Handler().postDelayed({
                kotlin.run {
                    mapTypeFAB.visibility = View.VISIBLE
                }
            }, 100)
            anim.start()
        }
    }

    // Handle selection of the Default map type
    map_type_default.setOnClickListener {
        map_type_default_background.visibility = View.VISIBLE
        map_type_satellite_background.visibility = View.INVISIBLE
        map_type_terrain_background.visibility = View.INVISIBLE
        map_type_default_text.setTextColor(Color.BLUE)
        map_type_satellite_text.setTextColor(Color.parseColor("#808080"))
        map_type_terrain_text.setTextColor(Color.parseColor("#808080"))
        map.mapType = GoogleMap.MAP_TYPE_NORMAL
    }

    // Handle selection of the Satellite map type
    map_type_satellite.setOnClickListener {
        map_type_default_background.visibility = View.INVISIBLE
        map_type_satellite_background.visibility = View.VISIBLE
        map_type_terrain_background.visibility = View.INVISIBLE
        map_type_default_text.setTextColor(Color.parseColor("#808080"))
        map_type_satellite_text.setTextColor(Color.BLUE)
        map_type_terrain_text.setTextColor(Color.parseColor("#808080"))
        map.mapType = GoogleMap.MAP_TYPE_SATELLITE
    }

    // Handle selection of the terrain map type
    map_type_terrain.setOnClickListener {
        map_type_default_background.visibility = View.INVISIBLE
        map_type_satellite_background.visibility = View.INVISIBLE
        map_type_terrain_background.visibility = View.VISIBLE
        map_type_default_text.setTextColor(Color.parseColor("#808080"))
        map_type_satellite_text.setTextColor(Color.parseColor("#808080"))
        map_type_terrain_text.setTextColor(Color.BLUE)
        map.mapType = GoogleMap.MAP_TYPE_TERRAIN
    }
}

End Result


This is my end result. Like I said, I am only a beginner, things could definitely be better, but for now, I'm very happy with the result.

enter image description here

The easiest solution i can think of right now is to:

  1. Add a <CardView> to your layout:
    ...
   <android.support.design.widget.FloatingActionButton 
      android:id="@+id/fab">  

   <android.support.v7.widget.CardView
      android:id="@+id/cardview"
      app:layout_constraintEnd_toEndOf="@+id/fab"
      app:layout_constraintTop_toTopOf="@+id/fab">
  1. Fill with Views for the dialog
  2. Set Visibility to View.GONE:

     <android.support.v7.widget.CardView
        android:id="@+id/cardview"
        app:layout_constraintEnd_toEndOf="@+id/fab"
        app:layout_constraintTop_toTopOf="@+id/fab"
        android:visibility="gone">
    
  3. In your activity set Visibility to View.Visibleon button click:

    fab.setOnClickListener{  
      cardview.visibility = View.VISIBLE
    }
    
Related