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:
TouchableWrapper class defining the UpdateMapAfterUserInterection interface
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
}
}
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
@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.
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
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?
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!
@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.
@Matias,
Time has been a very valuable commodity for me lately… publishing on GitHub is a good idea… will do at first opportunity.
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;
}
}
Thanks for sharing Dimitar. You saved me a lot of time.
Mike.
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
This would be better if you used MotionEvent.ACTION_MOVE xD
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
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.
Your solution is perfect, works like a charm ! Thanks.
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.
Nice job thank you. Now I have a an special case using your solution where my application crashes. I am using the navigation drawer and I only supoort Android 4.x. In navigation drawer all the activities are fragments, so my map is a fragment inside a fragment. I implement your code + Casper Fix and sadly the event is not working. Then, when I change between options at the navigation drawer the app crashes because a inflating error in the xml file (the fragment xml line exactly)
Oh, I forgot, I don’t use FragmentActivity and SupportMapFragment classes. I work with Fragment and MapFragment
Can someone explain to me what to pass in mTouchView.setUpdateMapAfterUserInterection(mUpdateMapAfterUserInterection);
I have implemented the interface inside my Fragment which extends a normal android.support.v4.app.Fragment
Thanks
I m facing an isssue and posted a complete quiz detail here on stack over flow. please can you answer this question
http://stackoverflow.com/questions/23239258/adding-pan-touch-drag-in-custom-supportmapfragment-in-sherlockfragment/24270542#24270542
thanks
Regards
Qadir Hussain
good stuff. helped me alot.
Pingback:Google map with doubleclick event and pan event in android
Works like a charm! Thanks!
Thank you for this, it works really well.
After updating v4
if you want to findFragmentById
– inside FragmentActivity : call getFragmentManager().find…
– inside Fragment : call getChildFragmentManager().find…
hi,
I tried exactly like this but got any results..The touch event never detected.I had to solve a problem same as Dave.But not able to find a solution with the code here ..please help.
Hi,
Can someone please confirm that this code is still working?
I had this code embedded to my app working like a charm but after an update I only get a white screen instead.
First of all thank you for this useful sharing.I am using your code in my project.But my ActivityMapv2 class extends fragments not fragmentActivity.I face a problem in class TouchableWrapper.
updateMapAfterUserInterection = (StationsFragment.getActivity()) context;
How can I handle this problem?
Aplause my friend !
Nice Work @Dimitar!
Nice simple solution. I wouldn’t say its hacky if we are given no other choice.
I am extending your solution a bit.
To secure the approach when flinging the map etc. I also implement onCameraChange and set flags for them both when camera is updated and user has done interacting. With this I am building a new listener in the fragment where I can signal the activity:
User started interaction (animate some views),
User stopped interaction (animate back)
Map done panning (fetch data and update clusters etc.)
Thank you!
“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…”
In the iOS Google Maps SDK, there are callback methods for “willMoveMap” and “didMoveMap”
hahahaha
Great job! This implementation looks great on my Google Map.
hi iam following your code its nice but when moving map iam making invisble to layout and after getting different address i make visible the problem is when i simply touch map the layout gets viisble how to avoid
hi iam following your code its nice but when moving map iam making invisble to layout and after getting different address i make visible the problem is when i simply touch map the layout gets INviisble how to avoid
Thank you. Working
Anybody know how to make this method distinguish between marker touch and non-marker touch on the underlying map? Is it just handled by returning false to the map and letting the listeners there handle it?