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();
}
});
Related
I haven't been able to find a question online similar to this, so I thought I would submit the question. In most cases it seems people have the opposite problem were lag may be occurring during a touch event, but I am seeing the exact opposite. I am creating an air hockey game and each frame the pieces are moved based on their current parameters, and the game is redrawn while in the background the override ontouch listener is looking for motion events. The thing is there is noticeable lag when there is no fingers touching the screen and just watching the animated pieces, but as long as there is any form of motion event happening, if ontouch is being called, then the animation is smooth. I really can not figure why this is, my best educated guess is the interrupt that is checking if ontouch should be called is consuming more resources than it should, but I know nothing about how to modify or check the ontouch interrupt behavior at all so hopefully someone might know.
A little more background on how everything is organized. The overall class is located in Main.java, it is created here and requests the Context view, the game class is an extension of ImageView. The game class has a #override OnTouchEvent, which has all the defintions for what to do depending on what state and what motion event. The class also has a draw method, and at the end of the method it calls h.postDelayed(r, FRAME_RATE); where r is a Runnable with #Override public void run() which just calls invalidate();. h is just a handler that is initialized in the class. So each time the game is drawn invalidate will be called after the FRAME_RATE elapse time of 10ms, which tells the game to redraw. All the move functions for the game are also called from the draw method. The OnTouch is happening all along side this process so why would it be smoother if on OnTouch is being called rather than checking if its true, it all seems counter intuitive but I'm sure there is a logical reason. Lastly the lag time was measurable using the system clock and was time dependent based on where exactly it was called. It just showed an increase in passing time between the games move function when a touch event was not occurring.
Sorry for the long response without much actual code context, I hope it is descriptive enough. The code its self is rather long so I didn't think it would help to post that, but if needed I can do better at re posting some pseudo code with the visualized hierarchy. Thank you for any help and suggestions for this problem. It would be great to be able to understand why it is happening.
Here is the code, I was trying to fit it into a comment didn't realize you had to post it by editing the original post.
public class Main extends Activity {
Game game;
private Menus menus; // contains menu info
int menu; //keep track of what menu you are at
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
game = new Game(this);
setContentView(game);
}
#Override
public void onDestroy() {
super.onDestroy();
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
..some code here to process photo files
}
//get the orientation of loaded pictures
public static int getOrientation(Context context, Uri photoUri) {
/* it's on the external media. */
Cursor cursor = context.getContentResolver().query(photoUri,
new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, null, null, null);
if (cursor.getCount() != 1) {
return -1;
}
cursor.moveToFirst();
return cursor.getInt(0);
}
public void Keyboard()
{
use keyboard listener and perform some actions if...
}
public class Game extends ImageView{
private Game game;
//Touch Events
boolean pressed;
private Context mContext;
private Handler h;
private final int FRAME_RATE = 10;
boolean initDimension = false;
//constant for defining the time duration between the click that can be considered as double-tap
static final int MAX_DURATION = 500;
Bitmap background;
int HEIGHT;
int WIDTH;
public Game(Context context) {
super(context);
mContext = context;
h = new Handler();
//ModeInit(); // load up puck game mode
menu = 0; //after tap from ontouch changes to menu = 1 for game
}
private Runnable r = new Runnable() {
#Override
public void run() {
invalidate();
}
};
#Override
public boolean onTouchEvent(MotionEvent e)
{
Switch Statement...
CHECK MOTION EVENT TYPE
CHECK menu state.. perform action
}
public void ModeInit()
{
game = new Game( mContext, WIDTH, HEIGHT );
}
protected void onDraw(Canvas c)
{
if(!initDimension)
{
WIDTH = this.getWidth();
HEIGHT = this.getHeight();
initDimension = true;
}
else
{
//logo screen
if(menu == 0)
{
menu = menus.DisplayIntro(c);
}
//game mode
else if(menu == 1)
{
game.paint_game(c);
long run_test = System.nanoTime()/1000;
game.Move();
Log.d("run time: ",": "+(System.nanoTime()/1000-run_test)); //Ontouch LAG TEST
}
... other menu items here
}
h.postDelayed(r, FRAME_RATE);
}
}
}
I'm making a brick breaking game. I coded that when I pressed back button in-game, the game turns back to the main menu. And when I touch the Start button, I want to re-create the game. But my ball isn't moving after timer_StartCompletely is passed. In other words, my timer_ball isn't working. I have this code in my onBackPressed:
#Override
public void onBackPressed()
{
if(status == INGAME) {
scene.detachChildren();
moveBall = false;
status = MENU;
ballX = (kamera.getWidth()/2)-(32/2);
ballY = (kamera.getHeight()/2)-(32/2);
ballSpeed = 3.5f;
cx = (kamera.getWidth()/2)-(cubukTex.getWidth()/2);
cy = kamera.getHeight()-25;
this.mEngine.unregisterUpdateHandler(timer_ball);
this.mEngine.unregisterUpdateHandler(timer_club);
timer_ball.reset();
musicBackground.play();
}
}
My timer declaration:
timer_StartCompletely = new TimerHandler(0.5f, new ITimerCallback() {
#Override
public void onTimePassed(final TimerHandler pTimerHandler) {
mEngine.unregisterUpdateHandler(pTimerHandler);
mEngine.registerUpdateHandler(timer_ball);
}
});
In my timer_ball, I coded movement of ball (the ball must move certainly, if timer_ball is called).
I have also a touch event that I control the touching buttons and registering timer_StartCompletely.
When instantiating your TimerHandler, you can pass a parameter called pAutoReset (You didn't pass it, so false is passed:
public TimerHandler(final float pTimerSeconds, final ITimerCallback pTimerCallback) {
this(pTimerSeconds, false /*pAutoReset*/, pTimerCallback);
}
This parameter decides whether the TimerHandler should automatically reset itself after the time has passed (Which means that if you pass true, the callbacks are repeated).
The problem here: You didn't pass true, neither called the reset method of the TimerHandler. So if we look at the relevant code in TimeHandler.java:
if(!this.mTimerCallbackTriggered) {
this.mTimerSecondsElapsed += pSecondsElapsed;
if(this.mTimerSecondsElapsed >= this.mTimerSeconds) {
this.mTimerCallbackTriggered = true;
this.mTimerCallback.onTimePassed(this);
}
}
Your callback executes once, and now mTimerCallbackTriggered is true, so it won't execute anymore.
Solution: Either call the reset method each time before registering the TimerHandler, or create it with pAutoReset = true.
I am developing a game using jbox2d in conjunction with jBox2d for android. I would like to detect if user touches a particular dynamic body among various bodies in my world. I have tried iterating over all the bodies and find one of my interest but it didnt work for me. Please help
Heres what I did :
#Override
public boolean ccTouchesEnded(MotionEvent event)
{
CGPoint location = CCDirector.sharedDirector().convertToGL(CGPoint.ccp(event.getX(),
event.getY()));
for(Body b = _world.getBodyList();b.getType()==BodyType.DYNAMIC; b.getNext())
{
CCSprite sprite = (CCSprite)b.getUserData();
if(sprite!=null && sprite instanceof CCSprite)
{
CGRect body_rect = sprite.getBoundingBox();
if(body_rect.contains(location.x, location.y))
{
Log.i("body touched","<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
expandAndStopBody(b);
break;
}
}
}
return true;
}
After the touch, system continues to print GC_CONCURRENT freed 1649K, 14% free 11130K/12935K, paused 1ms+2ms and everything goes to hung like state.
To check if a body is touched you can the method queryAABB of world object. I try to rearrange your code to use the method:
// to avoid creation every time you touch the screen
private QueryCallback qc=new QueryCallback() {
#Override
public boolean reportFixture(Fixture fixture) {
if (fixture.getBody()!=null)
{
Body b=fixture.getBody();
CCSprite sprite = (CCSprite)b.getUserData();
Log.i("body touched","<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
expandAndStopBody(b);
}
return false;
}
};
private AABB aabb;
#Override
public boolean ccTouchesEnded(MotionEvent event)
{
CGPoint location = CCDirector.sharedDirector().convertToGL(CGPoint.ccp(event.getX(), event.getY()));
// define a bounding box with width and height of 0.4f
aabb=new AABB(new Vec2(location.x-0.2f, location.y-0.2f),new Vec2(location.x+0.2f, location.y+0.2f));
_world.queryAABB(qc, aabb);
return true;
}
I try to reduce garbage collector, but something has to be instanziate to.
More info on http://www.iforce2d.net/b2dtut/world-querying
You should check to make sure the body is not null in your list, e.g.
for ( Body b = world.getBodyList(); b!=null; b = b.getNext() )
{
// do something
}
Not sure if this will solve the hang, but should do.
So in my current application, I want to zoom to a lat/long point, but also use the animateTo (or equivalent) to make sure the screen is properly centered. Currently I'm just doing something like this:
_mapController.animateTo(new GeoPoint(googleLat, googleLng));
// Smooth zoom handler
int zoomLevel = _mapView.getZoomLevel();
int targetZoomLevel = AppConstants.MAP_ZOOM_LEVEL_SWITCH;
long delay = 0;
while (zoomLevel++ < targetZoomLevel) {
handler.postDelayed(new Runnable() {
#Override
public void run() {
_mapController.zoomIn();
}
}, delay);
delay += 350; // Change this to whatever is good on the device
}
This kind of works, but what happens is that my thread starts, BEFORE the 'animate to' finishes, so it zooms in and then 'jumps' to display the correct center geoPoint. Is there a way to smoothly 'drill down' to a certain zoom level, as well as move to a geoPoint at the same time? Similar to how the official Googl eMaps application zooms to an address after you've searched for it is what I'm looking to do.
I would do something like:
_mapController.animateTo(new GeoPoint(googleLat,googleLong), new Runnable() {
public void run()
{
_mapController.zoomIn ();
}
});
This will achieve a pan-then-zoom effect. You can try your same logic inside the runnable to perform multi-step zoomIn's.
Could you please tell me how to capture the end of panning of MapView? At the beginning, I thought I could use the MotionEvent.ACTION_UP. However, when my touch is moved fast, the panning animation is still in progress while the ACTION_UP event is already fired before.
I had to come up with a solution to this problem also. It appears there is not a method in the android SDK to handle this event. What I did was create a timer that runs every 200ms and checks to see if the centerpoint of the mapview has changed. If it has and the a flag is set. The timer method knows when the panning has stopped when the centerpoint is no longer changing(oldMapCenter==newMapCenter). It may not be the best solution, but it is working for me. Insert the code below in your MapActivity.
NOTE: This will not update the view WHILE the user is panning, only AFTER.
GeoPoint newMapCenter,oldMapCenter;
int newZoomLevel=0,oldZoomLevel=0;
Boolean isMoving=false;
#Override
public void onResume() {
super.onResume();
autoUpdate = new Timer();
autoUpdate.schedule(new TimerTask() {
#Override
public void run() {
((Activity) getActivity()).runOnUiThread(new Runnable() {
public void run() {
if(oldMapCenter==null){
oldMapCenter=mapView.getMapCenter();
}else{
newMapCenter=mapView.getMapCenter();
if(!geoPointsAreSame(newMapCenter,oldMapCenter)&& (!isMoving)){
isMoving=true;
}else{
if(geoPointsAreSame(newMapCenter,oldMapCenter)&&(isMoving)){
//Insert update overlay code here
isMoving=false;
}
}
oldMapCenter=newMapCenter;
}
newZoomLevel=mapView.getZoomLevel();
if(oldZoomLevel==0){
oldZoomLevel=mapView.getZoomLevel();
}else{
if(oldZoomLevel!=newZoomLevel){
//insert update overlay code here
}
}
oldZoomLevel=mapView.getZoomLevel();
}
}
private boolean geoPointsAreSame(GeoPoint newMapCenter,
GeoPoint oldMapCenter) {
if(newMapCenter.getLatitudeE6()==oldMapCenter.getLatitudeE6()){
if(newMapCenter.getLongitudeE6()==oldMapCenter.getLongitudeE6()){
return true;
}
}
return false;
}
});
}
}, 3000, 200); // updates each 200 millisecs`-