Include a MapActivity, enabling the user to select the location of the placemark
As a user, create a new placemark with a title + image. Now edit the placemark and change the title - and update the placemark. Notice that the title has changed in PlacemarkList.
Edit the placemark and change the image - and update the placemark. Go back in to edit the placemark again - what image do you see?
There is a bug here - the placemark activity is still showing the original image. See if you can debug and fix this bug.
The problem is we are not changing the stored placemark in the update
function in the PlacemarkMemStore:
...
override fun update(placemark: PlacemarkModel) {
var foundPlacemark: PlacemarkModel? = placemarks.find { p -> p.id == placemark.id }
if (foundPlacemark != null) {
foundPlacemark.title = placemark.title
foundPlacemark.description = placemark.description
foundPlacemark.image = placemark.image
logAll();
}
}
...
Just one line added in the above.
in PlacemarkActivity, if you are editing a placemark OR if the placemark already has an image, change the button label to be 'Change Image' from
<string name="change_placemark_image">Change placemark image</string>
When edit it detected, change the label:
...
if (intent.hasExtra("placemark_edit")) {
...
placemarkImage.setImageBitmap(readImageFromPath(this, placemark.image))
if (placemark.image != null) {
chooseImage.setText(R.string.change_placemark_image)
}
...
When an image is loaded, also change the label:
...
IMAGE_REQUEST -> {
if (data != null) {
placemark.image = data.getData().toString()
placemarkImage.setImageBitmap(readImage(this, resultCode, data))
chooseImage.setText(R.string.change_placemark_image)
}
...
Introduce a new ImageView into the PlaceMarkCard view (displayed by PlacemarkListAcivity). Display the placemark images here (if it has one)
Extend the card to include an imageView:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:elevation="24dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="@+id/imageIcon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginRight="16dp"/>
<TextView
android:id="@+id/placemarkTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/imageIcon"
android:textSize="30sp"/>
<TextView
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/placemarkTitle"
android:layout_toRightOf="@+id/imageIcon"/>
</RelativeLayout>
</android.support.v7.widget.CardView>
In the adapter - initialise the imageView from the model:
...
fun bind(placemark: PlacemarkModel, listener : PlacemarkListener) {
itemView.placemarkTitle.text = placemark.title
itemView.description.text = placemark.description
itemView.imageIcon.setImageBitmap(readImageFromPath(itemView.context, placemark.image))
itemView.setOnClickListener { listener.onPlacemarkClick(placemark) }
}
...
First introduce a new button to set the location. This requires:
<string name="button_location">Set Location</string>
<Button
android:id="@+id/placemarkLocation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:background="@color/colorAccent"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:stateListAnimator="@null"
android:text="@string/button_location"
android:textColor="@color/colorPrimary"
android:textSize="16sp"/>
placemarkLocation.setOnClickListener {
info ("Set Location Pressed")
}
Build & run the application now and make sure the button appears as expected
and also the log appears when the button is pressed.
Briefly review then general guide to incorporating google maps here:
In Studio, select the activities package, and from the context menu insert a new Google Maps
activity:
This will suggest this configuration:
Accept all defaults and press Finish.
This will generate updates to the following files:
implementation 'com.google.android.gms:play-services-maps:15.0.1'
<string name="title_activity_maps">Map</string>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.wit.placemark">
<!--
The ACCESS_COARSE/FINE_LOCATION permissions are not required to use
Google Maps Android API v2, but you must specify either coarse or fine
location permissions for the 'MyLocation' functionality.
-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".main.MainApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".activities.PlacemarkActivity"></activity>
<activity android:name=".activities.PlacemarkListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!--
The API key for Google Maps-based APIs is defined as a string resource.
(See the file "res/values/google_maps_api.xml").
Note that the API key is linked to the encryption key used to sign the APK.
You need a different API key for each encryption key, including the release key that is used to
sign the APK for publishing.
You can define the keys for the debug and release targets in src/debug/ and src/release/.
-->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<activity
android:name=".activities.MapsActivity"
android:label="@string/title_activity_maps"></activity>
</application>
</manifest>
In Addition, these new files will have been generated:
<?xml version="1.0" encoding="utf-8"?>
<fragment 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:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.MapsActivity" />
package org.wit.placemark.activities
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
// Obtain the SupportMapFragment and get notified when the map is ready to be used.
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
/**
* Manipulates the map once available.
* This callback is triggered when the map is ready to be used.
* This is where we can add markers or lines, add listeners or move the camera. In this case,
* we just add a marker near Sydney, Australia.
* If Google Play services is not installed on the device, the user will be prompted to install
* it inside the SupportMapFragment. This method will only be triggered once the user has
* installed Google Play services and returned to the app.
*/
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
// Add a marker in Sydney and move the camera
val sydney = LatLng(-34.0, 151.0)
mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
}
<resources>
<!--
TODO: Before you run your application, you need a Google Maps API key.
To get one, follow this link, follow the directions and press "Create" at the end:
https://console.developers.google.com/flows/enableapi?apiid=maps_android_backend&keyType=CLIENT_SIDE_ANDROID&r=BC:AA:86:5A:D7:8C:52:EA:1C:F2:24:FB:80:2C:A6:73:1D:B4:DA:8B%3Borg.wit.placemark.activities
You can also add your credentials to an existing key, using these values:
Package name:
BC:AA:86:5A:D7:8C:52:EA:1C:F2:24:FB:80:2C:A6:73:1D:B4:DA:8B
SHA-1 certificate fingerprint:
BC:AA:86:5A:D7:8C:52:EA:1C:F2:24:FB:80:2C:A6:73:1D:B4:DA:8B
Alternatively, follow the directions here:
https://developers.google.com/maps/documentation/android/start#get-key
Once you have your key (it starts with "AIza"), replace the "google_maps_key"
string in this file.
-->
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">YOUR API KEY HERE</string>
</resources>
In the above file, you will need to insert your own API Key.
Before proceeding, you will need to get a google map api key. Follow the guide here:
Thus might take a little while, as you navigate through the various features available. The key will be a long string applicable to a range of google services. You can choose to restrict it to just the Maps API. Also, once you have inserted into the google_maps_api.xml
file, make sure you no NOT commit this file to any public repository (github). This key is intended to be a secret to your account. If you are using git, the simplest way of doing this is to append the following to .gitignore
:
app/src/release/res/values/google_maps_api.xml
app/src/debug/res/values/google_maps_api.xml
Rebuild the app - you may get an error:
Unresolved reference: R
If you get this error, include this import:
import org.wit.placemark.R
If all goes well, you should be able to start the maps activity:
placemarkLocation.setOnClickListener {
startActivity (intentFor<MapsActivity>())
}
If they key is correctly entered, the map should appear:
We might remove some of the additional comments generated by the studio wizard (read them first)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.wit.placemark">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".main.MainApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".activities.PlacemarkActivity"></activity>
<activity android:name=".activities.PlacemarkListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/google_maps_key" />
<activity
android:name=".activities.MapsActivity"
android:label="@string/title_activity_maps"></activity>
</application>
</manifest>
package org.wit.placemark.activities
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import org.wit.placemark.R
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
val sydney = LatLng(-34.0, 151.0)
mMap.addMarker(MarkerOptions().position(sydney).title("Marker in Sydney"))
mMap.moveCamera(CameraUpdateFactory.newLatLng(sydney))
}
}
Also, revise the default location + zoom level:
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
val wit = LatLng(52.245696, -7.139102)
mMap.addMarker(MarkerOptions().position(wit).title("Marker in Waterford"))
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(wit, 16f))
}
We would like to include the location into our model, so we can record the latitude/longitude the user selects. Introduce this new class alongside the PlacemarkModel class:
@Parcelize
data class Location(var lat: Double = 0.0,
var lng: Double = 0.0,
var zoom: Float = 0f) : Parcelable
Put this class in the same source file as PlacemarkModel.
We can now pass a location to the MapsActivity:
placemarkLocation.setOnClickListener {
val location = Location(52.245696, -7.139102, 15f)
startActivity (intentFor<MapsActivity>().putExtra("location", location))
}
Here is a revised version of MapsActivity - which uses the Location model
package org.wit.placemark.activities
...
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var mMap: GoogleMap
var location = Location()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
location = intent.extras.getParcelable<Location>("location")
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
val loc = LatLng(location.lat, location.lng)
mMap.addMarker(MarkerOptions().position(loc).title("Default Marker"))
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(loc, location.zoom))
}
}
Read this carefully, and note how the location object is being used. Specifically :
Experiment with different hard coded lat/lng + zoom values and verify that the MapsActivity correctly renders.
This is a slightly revised onMapReady implementation:
override fun onMapReady(googleMap: GoogleMap) {
mMap = googleMap
val loc = LatLng(location.lat, location.lng)
val options = MarkerOptions()
.title("Placemark")
.snippet("GPS : " + loc.toString())
.draggable(true)
.position(loc)
mMap.addMarker(options)
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(loc, location.zoom))
}
Try it out now.
You should be able to select the marker - it will display its lat/lng. And also able to drag it to a different location.
Finally, to keep the code more consistent, rename the map from mMap
to map
:
class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var map: GoogleMap
var location = Location()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_maps)
location = intent.extras.getParcelable<Location>("location")
val mapFragment = supportFragmentManager
.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
}
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
val loc = LatLng(location.lat, location.lng)
val options = MarkerOptions()
.title("Placemark")
.snippet("GPS : " + loc.toString())
.draggable(true)
.position(loc)
map.addMarker(options)
map.moveCamera(CameraUpdateFactory.newLatLngZoom(loc, location.zoom))
}
}
Currently, when you press back from the MapsActivity, any change made in the marker by the user is lost. We need to address this now.
First, add these fields to the ActivityPlacemark class:
val LOCATION_REQUEST = 2
var location = Location(52.245696, -7.139102, 15f)
When we start the map, we pass the LOCATION_REQUEST code:
placemarkLocation.setOnClickListener {
startActivityForResult(intentFor<MapsActivity>().putExtra("location", location), LOCATION_REQUEST)
}
Now, when a result is returned, we detect a LOCATION_REQUEST response, and recover the location:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
IMAGE_REQUEST -> {
if (data != null) {
placemark.image = data.getData().toString()
placemarkImage.setImageBitmap(readImage(this, resultCode, data))
chooseImage.setText(R.string.change_placemark_image)
}
}
LOCATION_REQUEST -> {
if (data != null) {
location = data.extras.getParcelable<Location>("location")
}
}
}
}
Try the app now - and drag the marker to a new location.
When you press back - and you get to the PlacemarkActivity - try pressing Set Location again. What happens? It still doesn't seem to remember the new location.
Introduce some logging statements to see what is happening. We will fix this in the next step.
To get this to work, we need further capabilities in MapsActivity.
First, implement a new interface:
class MapsActivity : AppCompatActivity(), OnMapReadyCallback, GoogleMap.OnMarkerDragListener {
This interface requires three methods:
override fun onMarkerDragStart(marker: Marker) {
}
override fun onMarkerDrag(marker: Marker) {
}
override fun onMarkerDragEnd(marker: Marker) {
location.lat = marker.position.latitude
location.lng = marker.position.longitude
location.zoom = map.cameraPosition.zoom
}
We are only interested in one of these methods for the moment.
We need modify the onMapReady method to make sure we are listening for this event:
override fun onMapReady(googleMap: GoogleMap) {
...
map.setOnMarkerDragListener(this)
...
}
Finally, we need to intercept the back button, and send it back to the parent activity:
override fun onBackPressed() {
val resultIntent = Intent()
resultIntent.putExtra("location", location)
setResult(Activity.RESULT_OK, resultIntent)
finish()
super.onBackPressed()
}
Try this now - and verify that if you change the location - and then immediately press back and then renter the map again. The location should be remembered, and the map centred to the new location.
Placemark application so far:
This archive does not include the Google Maps API keys - so will not build
If you create a new placemark - and set it location, note that when you click on the marker it shows its lat/lng. Move it around - and notice that the lat/lng in the panel never changes (even though it is at a different location).
See if you can fix this - such that it always shows the correct location.
(HINT: look up GoogleMap.OnMarkerClickListener
and setSnippet
)
When you leave the PlacemarkActiviy, the location is not currently stored in the PlacemarkModel correctly. So when you edit a placemark, it is back at the default location.
Fix this by making location part of the placemark model, so we can edit and change the locations for existing markers.
HINT: the simplest way of approach this might be to just extend the model to include three new fields:
@Parcelize
data class PlacemarkModel(var id: Long = 0,
var title: String = "",
var description: String = "",
var image: String = "",
var lat : Double = 0.0,
var lng: Double = 0.0,
var zoom: Float = 0f) : Parcelable
... and then manage these accordingly.