Saturday, June 21, 2014

Using Street View in Android with the StreetViewPanoramaFragment

    Google has released street view functionality in the most recent version of the Play Services, allowing Android developers another amazing feature in their maps skill set. I decided to dive right in because of my excitement for this feature, and luckily Google made it incredibly easy to implement street view into an app. The code for this sample can be found on my GitHub account. This app will take the user's current location and display their nearest street view panorama location.


    Before starting, we need to do the necessary project setup. The gradle file should import the latest version of Google Play Services, and the manifest should list necessary permissions and meta-data:

<uses-feature
    android:glEsVersion="0x00020000"
    android:required="true" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.ptrprograms.streetview.permission.MAPS_RECEIVE" />

<meta-data
    android:name="com.google.android.maps.v2.API_KEY"
    android:value="@string/maps_api_key" />
<meta-data
    android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version" />

compile 'com.google.android.gms:play-services:4.+'

    The easiest way to get street view into an app is by implementing a fragment. While the fragment can easily be extended, I chose to simply embed the pre-made StreetViewPanoramaFragment and use it for my main layout.

<fragment
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/street_view_panorama_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    class="com.google.android.gms.maps.StreetViewPanoramaFragment" />

    While I would usually implement initial code in onCreate, for this example I call a function from onRestore that starts the location client connection.

private void updateStreetViewPosition() {
    if( mLocationClient == null )
        mLocationClient = new LocationClient( this, this, this );
    if( !mLocationClient.isConnected() && !mLocationClient.isConnecting() ) {
        mLocationClient.connect();
    }
}

    Using the GooglePlayServicesClient.ConnectionCallbacks listeners, I save the user's current location and initialize the street view panorama view.

@Override
public void onConnected(Bundle bundle) {
    if( mCurrentLocation == null ) {
        mCurrentLocation = new LatLng( mLocationClient.getLastLocation().getLatitude(), mLocationClient.getLastLocation().getLongitude() );
    }
    initStreetView();
}

    In initStreetView(), I get a reference to the StreetViewPanormaFragment, and extract the StreetViewPanorama from it. StreetViewPanorama is the control structure for everything we will do in street view, and it can only be obtained from the fragment by using getStreetViewPanorama:

StreetViewPanoramaFragment fragment = ( (StreetViewPanoramaFragment) getFragmentManager().findFragmentById( R.id.street_view_panorama_fragment ) );
if( mPanorama == null ) {
    if( fragment != null ) {
        mPanorama = fragment.getStreetViewPanorama();    

    The next part of initStreetView() takes into account member variables that are used for keeping track of camera properties on rotation or other configuration changes. By using the StreetViewPanormaCamera.Builder object, I set the tilt, bearing and zoom levels to either the default values or the user's saved values, and then assign the camera to the panorama view.

if( mPanorama != null && mCurrentLocation != null ) {
    StreetViewPanoramaCamera.Builder builder = new StreetViewPanoramaCamera.Builder( mPanorama.getPanoramaCamera() );
    if( mBearing != builder.bearing )
        builder.bearing = mBearing;
    if( mTilt != builder.tilt )
        builder.tilt = mTilt;
    if( mZoom != builder.zoom )
        builder.zoom = mZoom;
    mPanorama.animateTo(builder.build(), 0);
    mPanorama.setPosition( mCurrentLocation, 300 );
    mPanorama.setStreetNamesEnabled( true );
}

    This is where other properties can be set to the street view, like setStreetNamesEnabled above, such restricting the user's ability to zoom, leave their current panorama or implement listeners for user interactions. Another important portion above is setPosition, as it must be set for the street view to work, and the accepted values are a LatLng or a LatLng with an integer. The second parameter tells the system to find the nearest street viewable point within a set number of meters from the provided LatLng.



    The final portion in this demo app is onSavedInstanceState, where I save the user location if they have explored a bit in street view, as well as the tilt, bearing and zoom of the camera, in order to not reset their view on rotate.

if( mPanorama != null && mPanorama.getLocation() != null && mPanorama.getLocation().position != null ) {
    outState.putDouble( EXTRA_LAT, mPanorama.getLocation().position.latitude );
    outState.putDouble( EXTRA_LONG, mPanorama.getLocation().position.longitude );
}
if( mPanorama != null && mPanorama.getPanoramaCamera() != null ) {
    outState.putFloat( EXTRA_TILT, mPanorama.getPanoramaCamera().tilt );
    outState.putFloat( EXTRA_BEARING, mPanorama.getPanoramaCamera().bearing );
    outState.putFloat( EXTRA_ZOOM, mPanorama.getPanoramaCamera().zoom );
}

    And with that, we have a basic street view placed into an app. This opens the door for a lot of developers to improve their apps with another great feature.