Browsing articles in "Blog"

Extending MapView to Add a Change Event

Oct 31, 2011   //   by Theo   //   Blog  //  5 Comments

The Problem

One of the most frequent mapping tasks in Android is creating a map which shows the location of several items. A critical part of getting this to work right is updating the map with new items each time the user has finished panning or zooming the map. Unfortunately Google Maps for Android does not provide a reliable built-in method to do this. In contrast, the Google Maps APIs v3 for JavaScript have zoom_changed() and center_changed() events specifically designed for this situation.

The Solution

This post shows you how to extend the Google Maps MapView to add an onChange listener that triggers after each time the user pans or zooms the map. This method works regardless of the way that a user interacts with the map. The user can:

  • Pan the map using a one finger swipe
  • Zoom the map using the plus and minus buttons on the zoom controls
  • Either zoom or zoom and pan at the same time using a two finger pinch or two finger reverse pinch motion

Implementation

To add the onChange event to the MapView class, we will create a MyMapView class that derives from MapView. We will override the computeScroll method that gets called internally when there are pan or zoom changes, and use that function to periodically reset a timed runnable. When the user stops making changes, computeScroll will no longer get called and the runnable will trigger. At that point the timer will fire the onChange event.

The complete code looks like this:

package com.bricolsoftconsulting.mapchange;

import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;

public class MyMapView extends MapView
{
	// ------------------------------------------------------------------------
	// LISTENER DEFINITIONS
	// ------------------------------------------------------------------------

	// Change listener
	public interface OnChangeListener
	{
		public void onChange(MapView view, GeoPoint newCenter, GeoPoint oldCenter, int newZoom, int oldZoom);
	}

	// ------------------------------------------------------------------------
	// MEMBERS
	// ------------------------------------------------------------------------

	private MyMapView mThis;
	private long mEventsTimeout = 250L; 	// Set this variable to your preferred timeout
	private boolean mIsTouched = false;
	private GeoPoint mLastCenterPosition;
	private int mLastZoomLevel;
	private Timer mChangeDelayTimer = new Timer();
	private MyMapView.OnChangeListener mChangeListener = null;

	// ------------------------------------------------------------------------
	// RUNNABLES
	// ------------------------------------------------------------------------
	private mOnChangeTask = new Runnable()
	{
		if (mChangeListener != null) mChangeListener.onChange(mThis, getMapCenter(), mLastCenterPosition, getZoomLevel(), mLastZoomLevel);
		mLastCenterPosition = getMapCenter();
		mLastZoomLevel = getZoomLevel();
	}
	// ------------------------------------------------------------------------
	// CONSTRUCTORS
	// ------------------------------------------------------------------------

	public MyMapView(Context context, String apiKey)
	{
		super(context, apiKey);
		init();
	}

	public MyMapView(Context context, AttributeSet attrs)
	{
		super(context, attrs);
		init();
	}

	public MyMapView(Context context, AttributeSet attrs, int defStyle)
	{
		super(context, attrs, defStyle);
		init();
	}

	private void init()
	{
		mThis = this;
		mLastCenterPosition = this.getMapCenter();
		mLastZoomLevel = this.getZoomLevel();
	}

	// ------------------------------------------------------------------------
	// GETTERS / SETTERS
	// ------------------------------------------------------------------------

	public void setOnChangeListener(MyMapView.OnChangeListener l)
	{
		mChangeListener = l;
	}

	// ------------------------------------------------------------------------
	// EVENT HANDLERS
	// ------------------------------------------------------------------------

	@Override
	public boolean onTouchEvent(MotionEvent ev)
	{
		// Set touch internal
		mIsTouched = (ev.getAction() != MotionEvent.ACTION_UP);

		return super.onTouchEvent(ev);
	}

	@Override
	public void computeScroll()
	{
		super.computeScroll();

		// Check for change
		if (isSpanChange() || isZoomChange())
		{
			// If computeScroll called before timer counts down we should drop it and
			// start counter over again
			resetMapChangeTimer();
		}
	}

	// ------------------------------------------------------------------------
	// TIMER RESETS
	// ------------------------------------------------------------------------

	private void resetMapChangeTimer()
	{
		MyMapView.this.removeCallbacks(mOnChangeTask);
		MyMapView.this.postDelayed(mOnChangeTask, mEventsTimeout);
	}

	// ------------------------------------------------------------------------
	// CHANGE FUNCTIONS
	// ------------------------------------------------------------------------

	private boolean isSpanChange()
	{
		return !mIsTouched && !getMapCenter().equals(mLastCenterPosition);
	}

	private boolean isZoomChange()
	{
		return (getZoomLevel() != mLastZoomLevel);
	}

}

Usage

To use the code in an activity, you need to:

  • Create a listener class derived from MyMapView.OnChangeListener that contains the onChange event handler
  • Provide an instance of this class to the MyMapView setOnChangeListener function.

The code below shows what’s required:

package com.bricolsoftconsulting.mapchange;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;

import android.os.Bundle;
import android.os.Handler;
import android.widget.Toast;

public class MyMapActivity extends MapActivity
{
	// Members
	MyMapView mMapView;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.map);

		// Populate the map member
		mMapView = (MyMapView) findViewById(R.id.theMap);

		// Add zoom controls
		mMapView.setBuiltInZoomControls(true);

		// Add listener
		mMapView.setOnChangeListener(new MapViewChangeListener());
	}

	private class MapViewChangeListener implements MyMapView.OnChangeListener
	{

		@Override
		public void onChange(MapView view, GeoPoint newCenter, GeoPoint oldCenter, int newZoom, int oldZoom)
		{
			// Check values
			if ((!newCenter.equals(oldCenter)) && (newZoom != oldZoom))
			{
				// Map Zoom and Pan Detected
				// TODO: Add special action here
			}
			else if (!newCenter.equals(oldCenter))
			{
				// Map Pan Detected
				// TODO: Add special action here
			}
			else if (newZoom != oldZoom)
			{
				// Map Zoom Detected
				// TODO: Add special action here
			}
		}
	}
}

Source Code

You can download a more complete example with source code at:

http://www.github.com/bricolsoftconsulting/mapchange/

As a bonus, the example also includes an implementation of a map overlay with onTap and onDoubleTap event handlers.

Credits

Thanks to Dave Smith for suggesting the use of View.postDelayed instead of a timer. His wonderful suggestion has been incorporated in this code.

Pages:«12

Blog Categories