How to detect a user pan/touch/drag on Android Map v2

If you are impatient, you can skip directly to the solution.

The new implementation of the Android Map (v2) is great in many ways and their support for map fragments is awesome, but they fall a bit short in a couple of minor but very necessary implementations. That makes the maps API a bit incomplete.

I am referring in particular to the ability to distinguish between a user dragging (panning) the map, verses a programmatic move of the map.

That looks like a very basic need that a lot of Android programmers would face when dealing with the new map implementation. I have a number of apps, where I would like to update the map after the user has panned the map to a new location and I want to do that ONLY after they had stopped moving the map. I don’t want to trigger multiple updates while they are in the process of panning.

As you can deduce so far, there is no straight up way to implement this very basic behavior… or at least I could not find one. So, if you have found one, please let me know… I am only human and I might have missed something!

There are a couple of listeners implemented that kind of give you something to work with at a first glance, but they are both incomplete.

We have onCameraChangeListener, which gets called every time the map moves, zooms in/out or the camera angle changes, but it is completely useless in helping us implement the above needed behavior. It does not tell you in any way if the map was moved due to a user interaction or due to some other event triggering the map’s “camera change”. Furthermore, if you implement this listener you will notice that it gets triggered multiple times during a map panning. So, this makes it completely inefficient, especially when you are fetching data over the network all the time. The implementation of this listener could have been a bit more complete, if the only added a boolean value to the GoogleMap.OnCameraChangeListener indicating if the action was done by a human or not. Just like they implemented the SeekBar.OnSeekBarChangeListener.

Furthermore, you might think that this situation can be remedied with the map’s setOnMapClickListener, but you would be wrong, as you cannot use it in any way to distinguish between a touch down and a touch up events.

That finally leads us down the path of implementing this basic map behavioral need ourselves! I would like to think that Google does this to us, because they know we are better programmers than those stinking iOS dudes, who are used to getting anything they need handed on a platter of APIs:-) But may be I am wrong again…

So, a basic search led me to a couple of stackoverflow posts that try to tackle this issue:

How to handle onTouch event for map in Google Map API v2?
Google maps android api v2 – detect touch on map

But that sill did not completely solve my problem due to a couple of reasons. One is that they use an ugly static boolean variable to communicate back to the map activity if the map was touched or not. Furthermore they use that boolean in the setOnCameraChangeListener to decide whether to update the map or not, which flat out does not work. And the reason it does not work is because in most of the cases the setOnCameraChangeListener listener is triggered before the boolean value was updated, resulting in a failure of the code after that to execute.

So I took all this and made a few small but significant changes, namely using a custom interface to trigger the map to update at the right time. The TouchableWrapper class declares a UpdateMapAfterUserInterection interface, which is implemented by the Map Activity. That gives us a very fine control over the map events and what we want to do with them. Now a complete disclaimer… This all works very well, but it is a hacky way of doing things. Extending the map fragment, then putting a frame layout on top of if to intercept user interaction and then passing it over to the map activity just feels (and is) hacky. But it is the best solution I could find or think of for now. Please share if you have a better solution!

Here is the complete implementation:

UpdateMapAfterUserInterection class


public  class TouchableWrapper extends FrameLayout {

	private long lastTouched = 0;
	private static final long SCROLL_TIME = 200L; // 200 Milliseconds, but you can adjust that to your liking
	private UpdateMapAfterUserInterection updateMapAfterUserInterection;

	public TouchableWrapper(Context context) {
		super(context);
		// Force the host activity to implement the UpdateMapAfterUserInterection Interface
		try {
			updateMapAfterUserInterection = (ActivityMapv2) context;
        } catch (ClassCastException e) {
            throw new ClassCastException(context.toString() + " must implement UpdateMapAfterUserInterection");
        }
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			lastTouched = SystemClock.uptimeMillis();
			break;
		case MotionEvent.ACTION_UP:
			final long now = SystemClock.uptimeMillis();
			if (now - lastTouched > SCROLL_TIME) {
				// Update the map
				updateMapAfterUserInterection.onUpdateMapAfterUserInterection();
			}
			break;
		}
		return super.dispatchTouchEvent(ev);
	}

	// Map Activity must implement this interface
    public interface UpdateMapAfterUserInterection {
        public void onUpdateMapAfterUserInterection();
    }
}

MySupportMapFragment class


public class MySupportMapFragment extends SupportMapFragment{
	public View mOriginalContentView;
	public TouchableWrapper mTouchView;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
		mOriginalContentView = super.onCreateView(inflater, parent, savedInstanceState);
		mTouchView = new TouchableWrapper(getActivity());
		mTouchView.addView(mOriginalContentView);
		return mTouchView;
	}

	@Override
	public View getView() {
		return mOriginalContentView;
	}
}

The layout for the map activity


<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapFragment"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_below="@+id/buttonBar"
    class="com.myFactory.myApp.MySupportMapFragment"
    />

And finally the Map Activity


public class ActivityMapv2 extends FragmentActivity implements UpdateMapAfterUserInterection {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }

// Implement the interface method
public void onUpdateMapAfterUserInterection() {
		// TODO Update the map now
        }
 }
Tuesday, April 9th, 2013 Android, Eclipse, Java, Programming

15 Comments to How to detect a user pan/touch/drag on Android Map v2

  1. Hi Dimitar,

    This is very good! I’m new to android development and cannot make your code work from the above. Will you publish your solution as an Eclipse Android Application Project to help me please?

    Thanks you again

    Dave

  2. Dave on April 21st, 2013
  3. @Dave,

    All you need is in the code I published in the post.

    You still need to set up your application with Google Services and the Google Maps API v2 first. Follow the guide here:

    https://developers.google.com/maps/documentation/android/intro

    Once you get the sample map fragment to show up in your project, then use my code above.

    If I sent you a project, it would not work for you. Enabling the Google Maps services is based on a package name and a keystore.

  4. dimitar on April 22nd, 2013
  5. Hi Dimitar,

    I finally got your solution working and as you said, it works very well! :-)

    I am wondering how to monitor map interactivity with your solution. I am creating the map in the class ActivityMapv2.

    I am thinking of creating a Boolean variable called MapNotMoving in the class ActivityMapv2. When the value is true I can fetch data over the network using ASYNC. While doing this if MapNotMoving changes to false I must stop fetching data over the network.

    Do you think this is the best way? If you would do it this way where would you setup this variable and in which class?

    Thanks again,
    Dave

  6. Dave on April 23rd, 2013
  7. Works like charm! The exact approach I was trying to achieved. Thanks!

    Why don’t you uploaded to a github repo so it is easily tracked by google and easier to find?

  8. Matías on April 24th, 2013
  9. Awesome blog! Do you have any hints for aspiring writers?
    I’m hoping to start my own site soon but I’m a little lost on
    everything. Would you recommend starting with a free platform
    like WordPress or go for a paid option? There are so many options out there that I’m completely confused .. Any recommendations? Thanks!

  10. guidepertutto.altervista.org on April 25th, 2013
  11. @Dave,

    I am not sure what the practical need for what you are asking would be. But I don’t think you need any boolean variable. To accomplish this, all you have to do is cancel any current AsyncTasks before you trigger a new one.

  12. dimitar on April 26th, 2013
  13. @Matias,

    Time has been a very valuable commodity for me lately… publishing on GitHub is a good idea… will do at first opportunity.

  14. dimitar on April 26th, 2013
  15. You are sooo close to doing it right

    never cast a context to a activity/fragment

    here is the correct way to do it
    Also added more contructors, so you can add it redirect from xml :)

    package com.crowdit.places.map;

    import android.content.Context;
    import android.os.SystemClock;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.widget.FrameLayout;

    public class TouchableWrapper extends FrameLayout {

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

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

    public TouchableWrapper(Context context) {
    super( context);
    }

    private long lastTouched = 0;
    private static final long SCROLL_TIME = 200L; // 200 Milliseconds, but you can adjust that to your liking
    private UpdateMapAfterUserInterection updateMapAfterUserInterection;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
    case MotionEvent.ACTION_DOWN:
    lastTouched = SystemClock.uptimeMillis();
    break;
    case MotionEvent.ACTION_UP:
    final long now = SystemClock.uptimeMillis();
    if (now – lastTouched > SCROLL_TIME) {
    // Update the map
    if(updateMapAfterUserInterection != null)
    updateMapAfterUserInterection.onUpdateMapAfterUserInterection();
    }
    break;
    }
    return super.dispatchTouchEvent(ev);
    }

    // Map Activity must implement this interface
    public interface UpdateMapAfterUserInterection {
    public void onUpdateMapAfterUserInterection();
    }

    public void setUpdateMapAfterUserInterection(UpdateMapAfterUserInterection mUpdateMapAfterUserInterection){
    this.updateMapAfterUserInterection = mUpdateMapAfterUserInterection;
    }
    }

  16. Casper on May 9th, 2013
  17. Thanks for sharing Dimitar. You saved me a lot of time.

    Mike.

  18. Mike Mellos on June 25th, 2013
  19. thanks Dimitar.I have implements your code in my practice but when I zoom it by two finger when i finger off.i return same as when begin.can you for me suggest

  20. Huong on September 13th, 2013
  21. This would be better if you used MotionEvent.ACTION_MOVE xD

  22. Jinoh on September 18th, 2013
  23. Thank you very much, this is very helpful.

    Casper or Dimitar,

    Would you mind maybe explaining why it’s better, if it is, to use Casper’s solution? I assume you then have to call setUpdateMapAfterUserInteraction from MySupportMapFragment, right?

    Thanks,

    James

  24. James on November 19th, 2013
  25. That seems help. When i stop moving the map, it can do the change at the right time.

    Could you tell me, the google has remove the onTouchEvent from the MapView? Because ,first i try to do the thing in onTouchEvent, but the onTouchEvent never run.

    I have search many try to find out the reason why the onTouchEvent can’t run.

  26. plaichat.wu on November 26th, 2013
  27. Your solution is perfect, works like a charm ! Thanks.

  28. Frank on January 19th, 2014
  29. Am I the only one who noticed that the interface is called “UpdateMapAfterUserInterEction” instead of “UpdateMapAfterUserInterAction”? XD

    BTW Thanks Dimitar for the code, you saved my day. And I agree with Casper fixes, I was already implementing them by myself.

  30. Giulio on February 4th, 2014

Leave a comment

*

Search

 

Archive

April 2014
M T W T F S S
« Feb    
 123456
78910111213
14151617181920
21222324252627
282930  

Other