Introduce a Google Map including necessary supporting resources such as Google Play Services Library and a personal API key. Additionally, support a landscape layout, validation of geolocation input, immediate mirroring any changes in this input in the view title and support for smaller small factor devices.
Here we shall embed a Google map in the ResidenceFragment class. This entails using:
The layout shall be refactored as shown here.
Additionally, we shall:
The instructions in this lab refer to development in debug mode. This means that the application you develop will not be suitable to publish on Play. Further information is available about this topic is available here and here.
It is necessary to add Google Play Services to the project.
Open the Android SDK Manager and install Google Play Services:
When Play Services installation is complete select menu commands:
<uses-sdk android:minSdkVersion="16"/>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
MyRent project has now been set up to reference Google Play Services.
Official documentation is available at the Google Maps Android API v2 page.
Important note: as an aid to portability it would help were you to locate the google-play-services_lib folder in the same containing folder as the MyRent project as shown here:
We recommend that you become familiar with the official documentation for Google Maps Android API v2.
It is necessary to obtain an API key before using the Google Maps API. This can be achieved as follows:
Retrieve your SHA-1 Fingerprint
Register a Google account (or use an existing one) and sign in.
Here is a summary of the data that should be retained securely:
In AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- The following two permissions are not required to use
Google Maps Android API v2, but are recommended. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="TODO: Insert your API key here" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
Replace the placeholder TODO: Inert your API key here with your key, created in an earlier step.
Verify that the Google Play Services library has been included by selecting the project name in Eclipse Package Explorer, right-click to open Properties window and then add google_play_services_lib as shown in Figure 1.
For reference here is the up-to-date manifest file:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.wit.myrent"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="19" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- The following two permissions are not required to use
Google Maps Android API v2, but are recommended. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<application
android:name=".app.MyRentApp"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".activities.ResidenceListActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.ResidencePagerActivity"
android:label="@string/app_name">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.ResidenceListActivity"/>
</activity>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="<TODO: Insert your API key here" />
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
</application>
</manifest>
Refactor the layout of the detail view:
Filename: fragment_residence.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="60"
android:baselineAligned="false"
android:orientation="vertical" >
<!-- LOCATION -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/location"
style="?android:listSeparatorTextViewStyle"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false" >
<!-- Geolocation (GPS Coords) -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="60"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@string/geolocation" />
<EditText
android:id="@+id/geolocation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/geolocation_hint" >
<requestFocus />
</EditText>
</LinearLayout>
<!-- Show Map Button -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="40"
android:orientation="vertical" >
<Button
android:id="@+id/show_map"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:enabled="false" />
</LinearLayout>
</LinearLayout>
<!-- STATUS -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/status"
style="?android:listSeparatorTextViewStyle"/>
<Button android:id="@+id/registration_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" />
<CheckBox
android:id="@+id/isrented"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="true"
android:focusable="false"
android:gravity="center"
android:text="@string/rented_checkbox_text"/>
<Button
android:id="@+id/tenant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:text="@string/landlord" />
<Button android:id="@+id/residence_reportButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:text="@string/residence_report"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:baselineAligned="false"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.88" />
</LinearLayout>
</LinearLayout>
Figure 2 should help you to understand the changes made here.
Before we introduce the map code we shall modify the Residence model class allowing it to store the current zoom level of the map.
Here are the code snippets to be added to Residence.java:
//New fields
public double zoom ;//zoom level of accompanying map
private static final String JSON_ZOOM = "zoom" ; //map zoom level
Constructors:
public Residence()
{
...
zoom = 16.0f;
}
public Residence(JSONObject json) throws JSONException
{
...
zoom = json.getDouble(JSON_ZOOM);
}
toJSON method
public JSONObject toJSON() throws JSONException
{
...
json.put(JSON_ZOOM , zoom);
return json;
}
Add this helper class to the org.wit.android.helpers package:
package org.wit.android.helpers;
import android.content.Context;
import com.google.android.gms.maps.model.LatLng;
public class MapHelper
{
public static LatLng latLng(Context context, String geolocation)
{
String[] g = geolocation.split(",");
if (g.length == 2)
{
return new LatLng(Double.parseDouble(g[0]), Double.parseDouble(g[1]));
}
return new LatLng(0, 0);
}
public static String latLng(LatLng geo)
{
return String.format("%.6f", geo.latitude) + ", " + String.format("%.6f", geo.longitude);
}
}
The method LatLng latLng(String geolocation) returns a LatLng object containing the latitude and longitude coordinates contained in the String geolocation.
The method String latLng(LatLng geo) returns a single string version of the coordinates contained in a LatLng object formed by concatenating the coordinates but separating them with a comma. For example:
52.253456,-7.187162
LatLng is a Google class representing a pair of latitude and longitude coordinates stored as degrees.
Please note that the use of a Context type as an argument above is not necessary at this time. We are including it here because it will be required later when we introduce validation and wish to generate Toast messages.
Add imports
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.SupportMapFragment;
Add a method to initialize the map fragment:
private void initializeMapFragment()
{
FragmentManager fm = getChildFragmentManager();
mapFragment = (SupportMapFragment) fm.findFragmentById(R.id.map);
if (mapFragment == null)
{
mapFragment = SupportMapFragment.newInstance();
fm.beginTransaction().replace(R.id.map, mapFragment).commit();
}
}
This fragment may be initialized only when the containing fragment has been created (ResidenceFragment).
@Override
public void onActivityCreated(Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
initializeMapFragment();
}
Disable mapButton and associated statements:
private Button mapButton;
mapButton = (Button) v.findViewById(R.id.show_map);
mapButton .setOnClickListener(this);
Add instance variables;
SupportMapFragment mapFragment;
GoogleMap gmap;
Marker marker;
LatLng markerPosition;
boolean markerDragged;
Note the boolean flag above (markerDragged).
We use the flag to avoid conflict that would otherwise arise as follows:
To avoid this potential cyclical behaviour, replace the afterTextChanged(Editable) method with the following:
@Override
public void afterTextChanged(Editable c)
{
String thisGeolocation = c.toString();
Log.i(this.getClass().getSimpleName(), "geolocation " + thisGeolocation);
residence.geolocation = thisGeolocation;
getActivity().setTitle(thisGeolocation);
// using a flag, markerDragged, to avoid race condition
if (markerDragged == true)
{
markerDragged = false;
}
else
{
renderMap(MapHelper.latLng(getActivity(), thisGeolocation));
}
}
Change class signature to implement:
GoogleMap.OnMarkerDragListener
GoogleMap.OnCameraChangeListener
New signature is:
public class ResidenceFragment extends SupportMapFragment implements TextWatcher,
OnCheckedChangeListener,
OnClickListener,
DatePickerDialog.OnDateSetListener,
GoogleMap.OnMarkerDragListener,
GoogleMap.OnCameraChangeListener
This change necessitates implementation of associated interface methods which you may use QuickFix to resolve:
Here are the methods which we have, where applicable, fully implemented. First are the implementations of the OnMarkerDragListener interface:
@Override
public void onMarkerDrag(Marker arg0)
{
}
@Override
public void onMarkerDragEnd(Marker arg0)
{
residence.geolocation = MapHelper.latLng(arg0.getPosition());
getActivity().setTitle(residence.geolocation);
gmap.animateCamera(CameraUpdateFactory.newLatLng(arg0.getPosition()));
markerDragged = true;
}
@Override
public void onMarkerDragStart(Marker arg0)
{
}
Here is the implementation of the OnCameraChangeListener:
@Override
public void onCameraChange(CameraPosition arg0)
{
residence.zoom = arg0.zoom;
markerPosition = MapHelper.latLng(getActivity(), residence.geolocation);
if (marker != null)
{
marker.remove();
marker = null;
}
marker = gmap.addMarker(new MarkerOptions().position(markerPosition).draggable(true).title("Residence").alpha(0.7f)
.snippet("GPS : " + markerPosition.toString()));
}
Next is the method to render the map:
private void renderMap(LatLng markerPosition)
{
if (mapFragment != null)
{
gmap = mapFragment.getMap();
if (gmap != null)
{
gmap.animateCamera(CameraUpdateFactory.newLatLngZoom(markerPosition, (float) residence.zoom));
gmap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
gmap.setOnMarkerDragListener(this);
gmap.setOnCameraChangeListener(this);
}
}
}
Next we override onStart() within which we invoke renderMap and register a listener to detect any changes to the map:
@Override
public void onStart()
{
super.onStart();
renderMap(MapHelper.latLng(getActivity(), residence.geolocation));
gmap.setOnCameraChangeListener(this);
}
Ensure that super is called in onCreateView:
super.onCreateView(inflater, parent, savedInstanceState);
Test the app as follows:
If you modify the geolocation such that the input becomes invalid then the app will crash. Here is example of invalid data:
We can solve this by introducing a try-catch block to MapHelper.latLng(Context, String) as follows:
package org.wit.android.helpers;
import android.content.Context;
import android.widget.Toast;
import com.google.android.gms.maps.model.LatLng;
import java.lang.NumberFormatException;
import static org.wit.android.helpers.LogHelpers.info;
public class MapHelper
{
public static LatLng latLng(Context context, String geolocation)
{
String[] g = geolocation.split(",");
try
{
if (g.length == 2)
{
return new LatLng(Double.parseDouble(g[0]), Double.parseDouble(g[1]));
}
}
catch (NumberFormatException e)
{
info(context, "Number format exception: invalid geolocation: " + e.getMessage());
}
Toast.makeText(context, "An invalid geolocation has been entered: defaulting to 0,0", Toast.LENGTH_SHORT).show();
return new LatLng(0, 0);
}
public static String latLng(LatLng geo)
{
return String.format("%.6f", geo.latitude) + ", " + String.format("%.6f", geo.longitude);
}
}
Add these import statements:
import java.lang.NumberFormatException;
import android.widget.Toast;
Test this validation as follows:
The final task in this step is to immediately mirror the geolocation in the title as the input is changed (as shown in Figure 3).
Simply add the following line of code at the end of onMarkerDragEnd.
geolocation.setText(residence.geolocation);
Run the app and test the features added above namely those that ensure that:
Make the data-input portion of the details view scrollable.
Official documentation on ScrollView is available here:
Here is the refactoted fragment_residence.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="60"
android:baselineAligned="false"
android:orientation="vertical" >
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/container"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<!-- LOCATION -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/location"
style="?android:listSeparatorTextViewStyle"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false" >
<!-- Geolocation (GPS Coords) -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="60"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@string/geolocation" />
<EditText
android:id="@+id/geolocation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/geolocation_hint" >
<requestFocus />
</EditText>
</LinearLayout>
<!-- Show Map Button -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="40"
android:orientation="vertical" >
<Button
android:id="@+id/show_map"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:enabled="false" />
</LinearLayout>
</LinearLayout>
<!-- STATUS -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/status"
style="?android:listSeparatorTextViewStyle"/>
<Button android:id="@+id/registration_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp" />
<CheckBox
android:id="@+id/isrented"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="true"
android:focusable="false"
android:gravity="center"
android:text="@string/rented_checkbox_text"/>
<Button
android:id="@+id/tenant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:text="@string/landlord" />
<Button android:id="@+id/residence_reportButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:text="@string/residence_report"/>
</LinearLayout>
</ScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="40"
android:baselineAligned="false"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
Replace the existing code with above and test that the details view is now scrollable:
<activity
android:name=".activities.ResidencePagerActivity"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden|adjustResize" >
Figure 1 should help you to understand the changes made here.
We shall now add a landscape layout.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" android:baselineAligned="false">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="60"
android:baselineAligned="false"
android:orientation="vertical" >
<ScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/container"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<!-- LOCATION -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/location"
style="?android:listSeparatorTextViewStyle"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
<!-- Geolocation (GPS Coords) -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="60"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@string/geolocation" />
<EditText
android:id="@+id/geolocation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/geolocation_hint" >
<requestFocus />
</EditText>
</LinearLayout>
<!-- Show Map Button -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="40"
android:orientation="vertical" >
<Button
android:id="@+id/show_map"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:enabled="false" />
</LinearLayout>
</LinearLayout>
<!-- STATUS -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/status"
style="?android:listSeparatorTextViewStyle"/>
<Button
android:id="@+id/registration_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"/>
<CheckBox
android:id="@+id/isrented"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="true"
android:focusable="false"
android:gravity="center"
android:text="@string/rented_checkbox_text"/>
<Button
android:id="@+id/tenant"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:text="@string/landlord" />
<Button android:id="@+id/residence_reportButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:text="@string/residence_report"/>
</LinearLayout>
</ScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="40"
android:baselineAligned="false"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
Test the scrollable feature in both portrait and landscape modes.