How to catch that map panning and zoom are really finished?
Asked Answered
G

3

10

I am trying to write an application that will dynamically load data to map while user pans or zooms it.

I need to track when the map is finished to change its view state (stops panning or zooming) and then load a new portion of data for creating markers. But in fact Google Maps API doesn't have any events to handle this.

There are some methods like creating an empty overlay to control onTouch events and so on, but map panning could last long after user finished his touch because GMaps use some kind of inertia to make the pan smoother.

I tried to subclass MapView but its draw() method is final thus it cannot be overridden.

Any ideas how to make precise handling of pan and zoom finishing?

Greedy answered 25/8, 2010 at 15:15 Comment(0)
G
12

Hours of researching and some decision was found. It has some cons and pros which I'll describe further.

The main thing we should do is to override some MapView's methods to handle its drawing behavior. In case we cannot override draw() method we should find another way in. There is another one derivative from View which may be overridden - computeScroll() method. It is called repeatedly as map continues padding. All we have to do is to set some time threshold to catch if computeScroll is not called anymore this time.
Here is what I did:

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 EnhancedMapView extends MapView {
public interface OnZoomChangeListener {
    public void onZoomChange(MapView view, int newZoom, int oldZoom);
}

public interface OnPanChangeListener {
    public void onPanChange(MapView view, GeoPoint newCenter, GeoPoint oldCenter);
}

private EnhancedMapView _this;

    // Set this variable to your preferred timeout
private long events_timeout = 500L;
private boolean is_touched = false;
private GeoPoint last_center_pos;
private int last_zoom;
private Timer zoom_event_delay_timer = new Timer();
private Timer pan_event_delay_timer = new Timer();

private EnhancedMapView.OnZoomChangeListener zoom_change_listener;
private EnhancedMapView.OnPanChangeListener pan_change_listener;


public EnhancedMapView(Context context, String apiKey) {
    super(context, apiKey);
    _this = this;
    last_center_pos = this.getMapCenter();
    last_zoom = this.getZoomLevel();
}

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

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

public void setOnZoomChangeListener(EnhancedMapView.OnZoomChangeListener l) {
    zoom_change_listener = l;
}

public void setOnPanChangeListener(EnhancedMapView.OnPanChangeListener l) {
    pan_change_listener = l;
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == 1) {
        is_touched = false;
    } else {
        is_touched = true;
    }

    return super.onTouchEvent(ev);
}

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

    if (getZoomLevel() != last_zoom) {
                    // if computeScroll called before timer counts down we should drop it and start it over again
        zoom_event_delay_timer.cancel();
        zoom_event_delay_timer = new Timer();
        zoom_event_delay_timer.schedule(new TimerTask() {
            @Override
            public void run() {
                zoom_change_listener.onZoomChange(_this, getZoomLevel(), last_zoom);
                last_zoom = getZoomLevel();
            }
        }, events_timeout);
    }

    // Send event only when map's center has changed and user stopped touching the screen
    if (!last_center_pos.equals(getMapCenter()) || !is_touched) {
        pan_event_delay_timer.cancel();
        pan_event_delay_timer = new Timer();
        pan_event_delay_timer.schedule(new TimerTask() {
            @Override
            public void run() {
                pan_change_listener.onPanChange(_this, getMapCenter(), last_center_pos);
                last_center_pos = getMapCenter();
            }
        }, events_timeout);
    }
}

}

Then you should register event handlers in your MapActivity like this:

public class YourMapActivity extends MapActivity {

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mv = new EnhancedMapView(this, "<your Maps API key here>");

    mv.setClickable(true);
    mv.setBuiltInZoomControls(true);

    mv.setOnZoomChangeListener(new EnhancedMapView.OnZoomChangeListener() {
        @Override
        public void onZoomChange(MapView view, int newZoom, int oldZoom) {
            Log.d("test", "zoom changed from " + oldZoom + " to " + newZoom);
        }
    }
    mv.setOnPanChangeListener(new EnhancedMapView.OnPanChangeListener() {
        public void onPanChange(MapView view, GeoPoint newCenter, GeoPoint oldCenter) {
            Log.d("test", "center changed from " + oldCenter.getLatitudeE6() + "," + oldCenter.getLongitudeE6() + " to " + newCenter.getLatitudeE6() + "," + newCenter.getLongitudeE6());
        }
    }
}

Now what about advantages and disadvantages of this approach?
Advantages:
- Events handles either way map was panned or zoomed. Touch event, hardware keys used, even programmatically fired events are handled (like setZoom() or animate() methods).
- Ability to skip unnecessary loading of data if user clicks zoom button several times quickly. Event will fire only after clicks will stop.
Disadvantages:
- It is quite not possible to cancel zooming or panning action (maybe I'll add this ability in the future)

Hope this little class will help you.

Greedy answered 27/8, 2010 at 9:35 Comment(4)
Another small issue I noticed when using this solution, is that changing the drawable for one of my OverlayItems or change rotation, computeScroll() would also be triggered, making the onPanChangeListener fire.Yaelyager
Not all your class constructors initialize the internals _this, last_center_pos, last_zoom. As far as I can tell, they should.Gaudy
(!last_center_pos.equals(getMapCenter()) || !is_touched) should be (!last_center_pos.equals(getMapCenter()) && !is_touched) else the pan listener fires continuously when the user does not touch the screen. You had it right in the comment above that line where you described the conditions using 'and'.Gaudy
I have created a small MapView class that has three events, onClick, onPan and onZoom here.Nightgown
A
0

The MapChange project, originally posted on a similar question here, helped me to fullfil the same task you asked for.

Alena answered 9/4, 2012 at 1:35 Comment(1)
As I see they use my technique. Maybe they were even inspired by my answer shown above :) Anyway I appreciated that somebody made it as a separate library.Greedy
C
0

Now this can be used:

googleMap.setOnCameraIdleListener {
                //Do your thing
            }

Callback interface for when camera movement has ended

Chiropteran answered 26/8, 2019 at 10:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.