I need to run a reverse geoCode request when the map has "settled". The closest thing I could find was the TransformListener:
mMap.addTransformListener(new Map.OnTransformListener() {
#Override
public void onMapTransformStart() {
}
#Override
public void onMapTransformEnd(MapState mapState) {
}
});
The problem is that onMapTransformEnd fires multiple times while the map is being dragged. Also tried adding a gesture listener to the mapview, but this doesn't seem to callback at the proper time either, as onPanEnd() isn't called until the user touches again.
mMapView.getMapGesture().addOnGestureListener(new MapGesture.OnGestureListener() {
#Override
public void onPanStart() {
}
#Override
public void onPanEnd() {
}
Is there a callback for when the map has finished moving and is settled?
Very quick answer, but if it is dragging specifically you are worried about, why not add your own hysteresis to OnTransformListener. I.e. a small wait time between onMapTransformEnd callback and when you execute your code. It would be canceled upon another onMapTransformStart callback.
The general callback for when the Map has settled is in fact onMapTransformEnd.
Ended up using a delay since the api didn't offer a settled listener:
mMap.addTransformListener(new Map.OnTransformListener() {
#Override
public void onMapTransformStart() {}
#Override
public void onMapTransformEnd(MapState mapState) {
// This works but is not optimal. I'd rather not wait a whole second before sending
// the geocoder request, but a half second is too fast to send only one request.
if(!mRunning) {
mRunning = true;
mSettledHandler.postDelayed(new Runnable() {
#Override
public void run() {
updatePickupViewOnMapSettled();
mRunning = false;
}
}, 1000);
}
}
});
I've created an app with a GoogleMap.
I want to do some maths and update the GoogleMap's markers inside a thread that runs all the time.
My thread needs to know the current googleMap's camera zoom and also the LatLngBounds to update the markers.
If I do that it works perfectly:
class updateMap implements Runnable{
private double currentZoom;
private LatLngBounds screenRegion;
#Override
public void run(){
while(!threadStop){//threadStop is a boolean to stop threads when onDestroy() is called
screenRegion = googleMap.getProjection().getVisibleRegion().latLngBounds;//fetches the screen's visible region
currentZoom = googleMap.getCameraPosition().zoom;
if(googleMap != null){//if my googleMap is fine
if(currentZoom >= 13){ //if the Camera's zoom is greater or equal than 13
//do some maths...
//updates the markers on the GoogleMap
}
}
}
}
}
but I know it's not recommended to interact with the UI thread directly within the thread so I've put the few lines of code that interact with the UI thread inside the method runOnUiThread() as followed but it doesn't work. My app freezes and nothing happens, ARN doesn't even show up !
class updateMap implements Runnable{
private double currentZoom;
private LatLngBounds screenRegion;
boolean map = false;
#Override
public void run(){
while(!threadStop){//threadStop is a boolean to stop threads when onDestroy() is called
runOnUiThread(new Runnable() {//Here I execute this piece of code on the UI thread to get the variables
#Override
public void run() {
if(googleMap != null){
map = true;
screenRegion = googleMap.getProjection().getVisibleRegion().latLngBounds;//fetches the screen's visible region
currentZoom = googleMap.getCameraPosition().zoom;
}
}
});
if(map){//if my googleMap is fine
if(currentZoom >= 13){ //if the Camera's zoom is greater or equal than 13
//do some maths...
runOnUiThread(new Runnable(){
#Override
public void run(){
//updates the markers on the GoogleMap
}
});
}
}
}
}
}
If anyone has any idea ? Maybe I don't respect the syntax or maybe I shouldn't use the runOnUiThread() method twice or something...
Thanks in advance.
I think using UI thread to get data is wrong in your case. Do it before separate thread start and pass as parameters.
I would like to hide the zoom controls on a timed interval. Is there something I can call in the API to set the controls to fade out like they did in v1? Currently I've written some code to hide the controls after a set interval but before I go any further with it I was curious if there was already a known and better way of going about this.
mTimer = new Timer();
mapFragment.setOnCameraChangeListener(new OnCameraChangeListener() {
#Override
public void onCameraChange(CameraPosition position) {
mapFragment.getUiSettings().setZoomControlsEnabled(true);
hideCameraOnInterval(5000);
}
});
And this is the method hideControlsOnInterval
public void hideCameraOnInterval(long milliseconds){
TimerTask tt = new TimerTask() {
#Override
public void run() {
getActivity().runOnUiThread(new Runnable() {
#Override
public void run() {
mapFragment.getUiSettings().setZoomControlsEnabled(false);
}
});
}
};
mTimer.schedule(tt, milliseconds);
}
If there is not a better solution than I will implement logic to restart the TimerTask if it is going to be scheduled again before the delay is reached. If anyone has either a better solution or a good way to restart the task please submit an answer.
I have an app where I need to have a delay after each touch in an ImageButton.
I tried the Thread.sleep() method, but I am not sure if this is the best way to deal with it.
What do you guys recommend?
Any help is appreciatted!
ONE MORE THING: I want the content of the onTouch() event to be fired THEN I want to delay "X" seconds the next onTouch() event. It's like to prevent the user to click too many times in the button.
Since all touch events are handled by UI thread, Thread.sleep() will block your UI thread which is (I hope) not what you are looking for.
I think the most correct way to solve your problem would be using postDelayed(Runnable, long) interface in your onClick handler which allows your to delay execution:
#Override
public void onClick(View v)
{
postDelayed(new Runnable()
{
#Override
public void run()
{
// do your stuff here
}
}, 10000); //10sec delay
}
UPDATE:
If you want user to prevent clicking too fast on your image view, I strongly recommend go with onClick rather than onTouch (unless there are serious reasons for that)
However, please see the code snippet which might help you:
private boolean blocked = false;
private Handler handler = new Handler();
#Override
public boolean onTouchEvent(MotionEvent event)
{
if (event.getAction() == MotionEvent.ACTION_DOWN)
{
if (!blocked)
{
blocked = true;
handler.postDelayed(new Runnable()
{
#Override
public void run()
{
blocked = false;
}
}, 1000);
} else
{
return false;
}
}
return super.onTouchEvent(event);
}
I need to detect when a MapView has been scrolled or zoomed, like the "moveend" event in the javascript API. I'd like to wait until the view has stopped moving, so I can then detect if I need to query my server for items withing the viewing rectangle, and if so send out a request. (actually I send a request for a slightly larger area than the viewing rectangle)
Obviously, I'd rather not send out a request for data if the view is still moving. But even worse is that I don't know that I need to send another request, leaving areas of the map missing markers.
Currently I am subclassing MapView and handling the onTouchEvent as follows:
public boolean onTouchEvent(android.view.MotionEvent ev) {
super.onTouchEvent (ev);
if (ev.getAction() == MotionEvent.ACTION_UP) {
GeoPoint center = getMapCenter();
int latSpan = getLatitudeSpan(), lngSpan = getLongitudeSpan();
/* (check if it has moved enough to need a new set of data) */
}
return true;
}
Problem is, I don't know if the view has stopped, since scrolling tends to have inertia and can keep going past the "ACTION_UP" event.
Is there some event I can tap into that will alert me when a mapview is done moving (or zooming)? If not, has anyone written logic to detect this? In theory I could make a guess by looking at all the actions, and set something to come along bit later and check it...but...that seems messy and a PITA. But if someone has already written it.... :)
This is the method I am using at the moment, I have used this and tested it, works well.
Just make sure you make your draw() method efficient. (Avoid GC in it).
//In map activity
class MyMapActivity extends MapActivity {
protected void onCreate(Bundle savedState){
setContent(R.layout.activity_map);
super.onCreate(savedSate);
OnMapMoveListener mapListener = new OnMapMoveListener(){
public void mapMovingFinishedEvent(){
Log.d("MapActivity", "Hey look! I stopped scrolling!");
}
}
// Create overlay
OnMoveOverlay mOnMoveOverlay = new OnMoveOverlay(mapListener);
// Add overlay to view.
MapView mapView = (MapView)findViewById(R.id.map_view);
// Make sure you add as the last overlay so its on the top.
// Otherwise other overlays could steal the touchEvent;
mapView.getOverlays().add(mOnMoveOverlay);
}
}
This is your OnMoveOverlay class
//OnMoveOverlay
class OnMoveOverlay extends Overlay
{
private static GeoPoint lastLatLon = new GeoPoint(0, 0);
private static GeoPoint currLatLon;
// Event listener to listen for map finished moving events
private OnMapMoveListener eventListener = null;
protected boolean isMapMoving = false;
public OnMoveOverlay(OnMapMoveListener eventLis){
//Set event listener
eventListener = eventLis;
}
#Override
public boolean onTouchEvent(android.view.MotionEvent ev)
{
super.onTouchEvent(ev);
if (ev.getAction() == MotionEvent.ACTION_UP)
{
// Added to example to make more complete
isMapMoving = true;
}
//Fix: changed to false as it would handle the touch event and not pass back.
return false;
}
#Override
public void draw(Canvas canvas, MapView mapView, boolean shadow)
{
if (!shadow)
{
if (isMapMoving)
{
currLatLon = mapView.getProjection().fromPixels(0, 0);
if (currLatLon.equals(lastLatLon))
{
isMapMoving = false;
eventListener.mapMovingFinishedEvent();
}
else
{
lastLatLon = currLatLon;
}
}
}
}
public interface OnMapMoveListener{
public void mapMovingFinishedEvent();
}
}
Just implement your own listener eventListener.mapMovingFinishedEvent(); and fire the map moving bool by another method like above and your sorted.
The idea is when the map is moving the pixel projection to the coords will be changing, once they are the same, you have finished moving.
I have updated this with newer more complete code, there was an issue with it double drawing.
We don't do anything on the shadow pass as we would just double calculate per draw pass which is a waste.
Feel Free to ask any questions :)
Thanks,
Chris
I had the same problem and "solved" it in a similar way, but I think less complicated:
As overriding computeScroll() didn't work for me, I overrode onTouchEvent, too. Then I used a Handler, that invokes a method call after 50ms, if the map center changed, the same happens again, if the map center didn't change, the listener is called. The method I invoke in onTouchEvent looks like this:
private void refreshMapPosition() {
GeoPoint currentMapCenter = getMapCenter();
if (oldMapCenter==null || !oldMapCenter.equals(currentMapCenter)) {
oldMapCenter = currentMapCenter;
handler.postDelayed(new Runnable() {
#Override
public void run() {
refreshMapPosition();
}
}, 50);
}
else {
if (onScrollEndListener!=null)
onScrollEndListener.onScrollEnd(currentMapCenter);
}
}
But I'm waiting for a real solution for this, too ...
I don't really have a satisfactory solution to this problem, but I can tell what I did to partially solve it.
I subclassed MapView and overrode the computeScroll() method, which gets the current centre-point of the map and compares it with the last-known centre-point (stored as a volatile field in the subclass). If the centre-point has changed, it fires an event to the listener of the map (I defined a custom listener interface for this).
The listener is an activity that instantiates a subclass of AsyncTask and executes it. This task pauses for 100ms in its doInBackGround() method, before performing the server data fetch.
When the listener activity receives a second map-move event (which it will do because of the stepping effect of the map movement), it checks the status of the just-executed AsyncTask. If that task is still running, it will cancel() it. It then creates a new task, and executes that.
The overall effect is that when the listeners get the flurry of map-moved events a few milliseconds apart, the only one that actually triggers the task to perform the server-fetch is the last one in the sequence. The downside is that it introduces a slight delay between the map movement happening, and the server fetch occurring.
I'm not happy with it, it's ugly, but it mitigates the problem. I would love to see a better solution to this.
I solved it using a thread and it seems to work quite good. It not only detects center changes but also zoom changes. Well, the detection is done after zooming and scrolling ends. If you need to detect zooming changes when you move up the first finger then you can modify my code a bit to detect different pointers. But I didn't need it, so didn't include it and left some homework for you :D
public class CustomMapView extends MapView {
private GeoPoint pressGP;
private GeoPoint lastGP;
private int pressZoom;
private int lastZoom;
public boolean onTouchEvent( MotionEvent event ) {
switch( event.getAction() ) {
case MotionEvent.ACTION_DOWN:
pressGP = getMapCenter();
pressZoom = getZoomLevel();
break;
case MotionEvent.ACTION_UP:
lastGP = getMapCenter();
pressZoom = getZoomLevel();
if( !pressGP.equals( lastGP ) ) {
Thread thread = new Thread() {
public void run() {
while( true ) {
try {
Thread.sleep( 100 );
} catch (InterruptedException e) {}
GeoPoint gp = getMapCenter();
int zl = getZoomLevel();
if( gp.equals( lastGP ) && zl == lastZoom)
break;
lastGP = gp;
lastZoom = zl;
}
onMapStop( lastGP );
}
};
thread.start();
}
break;
}
return super.onTouchEvent( event );
}
public void onMapStop( GeoPoint point , int zoom ){
// PUT YOUR CODE HERE
}
}
With the latest version of google maps API (V2) there is a listener to do this, i.e. GoogleMap.OnCameraChangeListener.
mGoogleMap.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener()
{
#Override
public void onCameraChange(CameraPosition cameraPosition)
{
Toast.makeText(mActivity, "Longitude : "+cameraPosition.target.longitude
+", Latitude : "+cameraPosition.target.latitude, Toast.LENGTH_SHORT).show();
}
});