Chapters

Hide chapters

Android Apprentice

Third Edition · Android 10 · Kotlin 1.3 · Android Studio 3.6

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section II: Building a List App

Section 2: 7 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

15. Google Places
Written by Namrata Bandekar

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Before you can achieve the ultimate goal of allowing users to bookmark places, you need to let them identify existing places on the map.

In this chapter, you’ll learn how to identify when a user taps on a place and use the Google Places API to retrieve detailed information about the place.

Getting started

If you’re following along with your own app, open it and keep using it with this chapter. If not, don’t worry. Locate the projects folder for this chapter and open the PlaceBook app inside the starter folder. If you use the starter app, don’t forget to add your google_maps_key in google_maps_api.xml. Read Chapter 13 for more details about the Google Maps key.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

If you’re following along with your own app, you’ll also need to copy default_photo.png from src/main/res/drawable-xxx, which is included with the starter project, into your project:

Make sure to copy the files from all of the drawable folders (hdpi,mdpi,xhdpi,xxhdpi).

Before using the Google Places API, you need to take care of a bit of housekeeping first by enabling the Places API in the developer console and adding the Places API dependency.

Note: The Google screens in this book might be slightly different than what you see on the Google developer portal since Google changes these often.

Enable the places API

The Maps SDK for Android was enabled on your Google developer account when you created the initial Google Maps key. However, you need to turn on the Google Places API manually.

Places API overview

The Google Places API provides a wealth of capabilities all related to — wait for it — working with places on a map! A place is anything that can be identified on a map, such as a household, a business or a public park. Google places gives you access to over 100 million places stored in the main Google Maps database.

Add the Places API dependency

Just like the location API, you’ll have to add the Places API dependency yourself.

implementation "com.google.android.libraries.places:places:2.2.0"

Selecting points of interest

You may have noticed icons with place names scattered throughout the map. These are called points of interest, or POIs, and they will let the user look up details about each place. You’ll begin by making the POIs a little more interesting by allowing the user to interact with them.

map.setOnPoiClickListener {
  Toast.makeText(this, it.name, Toast.LENGTH_LONG).show()
}

Load place details

Now that you have the placeId when a user taps a POI, you can use it to look up more details about the place. The goal is to provide the user with a quick popup info window, from which they can decide if they want to bookmark the place.

import com.google.android.libraries.places.api.Places
import com.google.android.libraries.places.api.net.PlacesClient
private lateinit var placesClient: PlacesClient
private fun setupPlacesClient() {
  Places.initialize(getApplicationContext(), getString(R.string.google_maps_key));
  placesClient = Places.createClient(this);
}
setupPlacesClient()
private fun displayPoi(pointOfInterest: PointOfInterest) {
  // 1
  val placeId = pointOfInterest.placeId

  // 2
  val placeFields = listOf(Place.Field.ID,
      Place.Field.NAME,
      Place.Field.PHONE_NUMBER,
      Place.Field.PHOTO_METADATAS,
      Place.Field.ADDRESS,      
      Place.Field.LAT_LNG)

  // 3    
  val request = FetchPlaceRequest
      .builder(placeId, placeFields)
      .build()

  // 4
  placesClient.fetchPlace(request)
      .addOnSuccessListener { response ->
    // 5    
    val place = response.place
    Toast.makeText(this,
        "${place.name}, " +
        "${place.phoneNumber}",
        Toast.LENGTH_LONG).show()    
  }.addOnFailureListener { exception ->
    // 6
    if (exception is ApiException) {
      val statusCode = exception.statusCode
      Log.e(TAG,
          "Place not found: " +
          exception.message + ", " +
          "statusCode: " + statusCode)
    }
  }
}
map.setOnPoiClickListener {
  displayPoi(it)
}

Refactoring in Android Studio

Youʼll place each main step in its own method to keep things nice and clean. You start by refactoring displayPoi() to kick off the first step. You take the code inside of displayPoi() and move it into a new method that takes a single argument. You then add a call to the new method inside displayPoi(). This is a common refactoring step that Android Studio can automate for you.

private fun displayPoi(pointOfInterest: PointOfInterest) {
  displayPoiGetPlaceStep(pointOfInterest)
}

private fun displayPoiGetPlaceStep(pointOfInterest: PointOfInterest) {
  val placeId = pointOfInterest.placeId

  val placeFields = listOf(Place.Field.ID,
      Place.Field.NAME,
      Place.Field.PHONE_NUMBER,
      Place.Field.PHOTO_METADATAS,
      Place.Field.ADDRESS,      
      Place.Field.LAT_LNG)

  val request = FetchPlaceRequest
      .builder(placeId, placeFields)
      .build()

  placesClient.fetchPlace(request)
      .addOnSuccessListener { response ->
    val place = response.place
    Toast.makeText(this,
        "${place.name}, " +
        "${place.phoneNumber}",
        Toast.LENGTH_LONG).show()    
  }.addOnFailureListener { exception ->
    if (exception is ApiException) {
      val statusCode = exception.statusCode
      Log.e(TAG,
          "Place not found: " +
          exception.message + ", " +
          "statusCode: " + statusCode)
    }
  }
}

Fetching a place photo

Now, you’ll add a step to retrieve a photo using the place details you requested in the previous step.

private fun displayPoiGetPhotoStep(place: Place) {
  // 1
  val photoMetadata = place
      .getPhotoMetadatas()?.get(0)
  // 2    
  if (photoMetadata == null) {
    // Next step here
    return
  }            
  // 3
  val photoRequest = FetchPhotoRequest
      .builder(photoMetadata)
      .setMaxWidth(resources.getDimensionPixelSize(
          R.dimen.default_image_width))
      .setMaxHeight(resources.getDimensionPixelSize(
          R.dimen.default_image_height))
      .build()
  // 4    
  placesClient.fetchPhoto(photoRequest)
      .addOnSuccessListener { fetchPhotoResponse ->
    val bitmap = fetchPhotoResponse.bitmap
    // Next step here
  }.addOnFailureListener { exception ->
    if (exception is ApiException) {
      val statusCode = exception.statusCode
      Log.e(TAG,
          "Place not found: " +
          exception.message + ", " +
          "statusCode: " + statusCode)
    }
  }
}
  displayPoiGetPhotoStep(place)

Add a place marker

Finally, add a step to display a marker with the place details and photo. Add the following new method to MapsActivity:

private fun displayPoiDisplayStep(place: Place, photo: Bitmap?) {
  val iconPhoto = if (photo == null) {
    BitmapDescriptorFactory
        .defaultMarker()
  } else {
    BitmapDescriptorFactory.fromBitmap(photo)
  }

  map.addMarker(MarkerOptions()
      .position(place.latLng as LatLng)
      .icon(iconPhoto)
      .title(place.name)
      .snippet(place.phoneNumber)
  )
}
displayPoiDisplayStep(place, null)
displayPoiDisplayStep(place, bitmap)

Custom info window

Now you’re making some progress! The user can tap places to view a photo and details, but having large photos all over the map is a little unwieldy. A better experience would be to display a standard marker next to each place and only show the photo and details in a popup info window.

InfoWindowAdapter class

To create a custom info window, you create a class that conforms to the InfoWindowAdapter interface and then call map.setInfoWindowAdapter() with an instance of the class.

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="5dp">

  <ImageView
      android:id="@+id/photo"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginEnd="5dp"
      android:adjustViewBounds="true"
      android:maxWidth="200dp"
      android:scaleType="fitStart"
      android:contentDescription="@string/bookmark_image"
      android:src="@drawable/default_photo"/>

  <LinearLayout
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:orientation="vertical">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:textColor="#ff000000"
        android:textSize="14sp"
        android:textStyle="bold"
        tools:text="Place Title"/>

    <TextView
        android:id="@+id/phone"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="#ff7f7f7f"
        android:textSize="12sp"
        tools:text="555-121-1212"/>

  </LinearLayout>
</LinearLayout>
<string name="bookmark_image">Bookmark image</string>

// 1
class BookmarkInfoWindowAdapter(context: Activity) :
    GoogleMap.InfoWindowAdapter {

  // 2
  private val contents: View

  // 3
  init {
    contents = context.layoutInflater.inflate(
        R.layout.content_bookmark_info, null)
  }

  // 4
  override fun getInfoWindow(marker: Marker): View? {
    // This function is required, but can return null if
    // not replacing the entire info window
    return null
  }

  // 5
  override fun getInfoContents(marker: Marker): View? {
    val titleView = contents.findViewById<TextView>(R.id.title)
    titleView.text = marker.title ?: ""

    val phoneView = contents.findViewById<TextView>(R.id.phone)
    phoneView.text = marker.snippet ?: ""

    return contents
  }
}

Assigning the InfoWindowAdapter

In MapsActivity.kt, add the following line to onMapReady() after map is assigned:

map.setInfoWindowAdapter(BookmarkInfoWindowAdapter(this))
 map.addMarker(MarkerOptions()
     .position(place.latLng as LatLng)
     .title(place.name)
     .snippet(place.phoneNumber)
 )

Marker tags

So, how do you associate the image with the marker? There are several ways to tackle this problem, but they all involve using the tag property of the Marker object.

val marker = map.addMarker(MarkerOptions()
    .position(place.latLng as LatLng)
    .title(place.name)
    .snippet(place.phoneNumber)
)
marker?.tag = photo
val imageView = contents.findViewById<ImageView>(R.id.photo)
imageView.setImageBitmap(marker.tag as Bitmap?)

Where to go from here?

Pat yourself on the back for making it this far! You have everything you need to move on to the bookmarking feature.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now