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?
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.