We're building an application which is using the google maps api for android.
I have my MapController and MapView, and I enable the built-in zoom controls using:
mapView.setBuiltInZoomControls(true);
I would now like to get an event when the user actually zooms on the map, how do I go about that? I can find no such event or any general event where I could detect a change in zoom level.
Update
The mapView.getZoomControls() is deprecated. And the documentation suggests using mapView.setBuiltInZoomControls(bool) instead. This is okay, but I simply cannot figure out how to act on events from the built in zoom controls.
With the Google Maps Android API v2 you can use a GoogleMap.OnCameraChangeListener like this:
mMap.setOnCameraChangeListener(new OnCameraChangeListener() {
private float currentZoom = -1;
#Override
public void onCameraChange(CameraPosition pos) {
if (pos.zoom != currentZoom){
currentZoom = pos.zoom;
// do you action here
}
}
});
You can implement your own basic "polling" of the zoom value to detect when the zoom has been changed using the android Handler.
Using the following code for your runnable event. This is where your processing should be done.
private Handler handler = new Handler();
public static final int zoomCheckingDelay = 500; // in ms
private Runnable zoomChecker = new Runnable()
{
public void run()
{
checkMapIcons();
handler.removeCallbacks(zoomChecker); // remove the old callback
handler.postDelayed(zoomChecker, zoomCheckingDelay); // register a new one
}
};
Start a callback event using
protected void onResume()
{
super.onResume();
handler.postDelayed(zoomChecker, zoomCheckingDelay);
}
and stop it when you're leaving the activity using
protected void onPause()
{
super.onPause();
handler.removeCallbacks(zoomChecker); // stop the map from updating
}
Article this is based on can be found here if you want a longer write up.
I use this library which has an onZoom function listener. http://code.google.com/p/mapview-overlay-manager/. Works well.
As the OP said, use the onUserInteractionEvent and do the test yourself.
Here is a clean solution. Simply add this TouchOverlay private class to your activity and a method called onZoom (that is called by this inner class).
Note, you'll have to add this TouchOverlay to your mapView e.g.
mapView.getOverlays().add(new TouchOverlay());
It keeps track of the zoom level whenever the user touches the map e.g. to double-tap or pinch zoom and then fires the onZoom method (with the zoom level) if the zoom level changes.
private class TouchOverlay extends com.google.android.maps.Overlay {
int lastZoomLevel = -1;
#Override
public boolean onTouchEvent(MotionEvent event, MapView mapview) {
if (event.getAction() == 1) {
if (lastZoomLevel == -1)
lastZoomLevel = mapView.getZoomLevel();
if (mapView.getZoomLevel() != lastZoomLevel) {
onZoom(mapView.getZoomLevel());
lastZoomLevel = mapView.getZoomLevel();
}
}
return false;
}
}
public void onZoom(int level) {
reloadMapData(); //act on zoom level change event here
}
The android API suggests you use ZoomControls which has a setOnZoomInClickListener()
To add these zoom controls you would:
ZoomControls mZoom = (ZoomControls) mapView.getZoomControls();
And then add mZoom to your layout.
You Can use
ZoomButtonsController zoomButton = mapView.getZoomButtonsController();
zoomButton.setOnZoomListener(listener);
hope it helps
I think there might another answer to this question. The Overlay class draw method is called after any zoom and I believe this is called after the zoom level changes. Could you not architect your app to take advantage of this? You could even use a dummy overlay just to detect this if you wanted to. Would this not be more efficient than using a runnable with a delay?
#Override
public boolean draw (Canvas canvas, MapView mapView, boolean shadow, long when) {
int zoomLevel = mapView.getZoomLevel();
// do what you want with the zoom level
return super.draw(canvas, mapView, shadow, when);
}
For pre-V2.0, I made a class which extends MapView that alerts a listener with events when the map region starts changing (onRegionBeginChange) and stops changing (onRegionEndChange).
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 ExMapView extends MapView {
private static final String TAG = ExMapView.class.getSimpleName();
private static final int DURATION_DEFAULT = 700;
private OnRegionChangedListener onRegionChangedListener;
private GeoPoint previousMapCenter;
private int previousZoomLevel;
private int changeDuration; // This is the duration between when the user stops moving the map around and when the onRegionEndChange event fires.
private boolean isTouched = false;
private boolean regionChanging = false;
private Runnable onRegionEndChangeTask = new Runnable() {
public void run() {
regionChanging = false;
previousMapCenter = getMapCenter();
previousZoomLevel = getZoomLevel();
if (onRegionChangedListener != null) {
onRegionChangedListener.onRegionEndChange(ExMapView.this, previousMapCenter, previousZoomLevel);
}
}
};
public ExMapView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ExMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public ExMapView(Context context, String apiKey) {
super(context, apiKey);
init();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
isTouched = event.getAction() != MotionEvent.ACTION_UP;
return super.onTouchEvent(event);
}
#Override
public void computeScroll() {
super.computeScroll();
// If the map region is still changing (user is still scrolling or zooming), reset timer for onRegionEndChange.
if ((!isTouched && !getMapCenter().equals(previousMapCenter)) || (previousZoomLevel != getZoomLevel())) {
// If the region has just begun changing, fire off onRegionBeginChange event.
if (!regionChanging) {
regionChanging = true;
if (onRegionChangedListener != null) {
onRegionChangedListener.onRegionBeginChange(this, previousMapCenter, previousZoomLevel);
}
}
// Reset timer for onRegionEndChange.
removeCallbacks(onRegionEndChangeTask);
postDelayed(onRegionEndChangeTask, changeDuration);
}
}
private void init() {
changeDuration = DURATION_DEFAULT;
previousMapCenter = getMapCenter();
previousZoomLevel = getZoomLevel();
}
public void setOnRegionChangedListener(OnRegionChangedListener listener) {
onRegionChangedListener = listener;
}
public void setChangeDuration(int duration) {
changeDuration = duration;
}
public interface OnRegionChangedListener {
public abstract void onRegionBeginChange(ExMapView exMapView, GeoPoint geoPoint, int zoomLevel);
public abstract void onRegionEndChange(ExMapView exMapView, GeoPoint geoPoint, int zoomLevel);
}
}
I used a mixture of the above. I found that using the timmer to start a thread every half a second cause the map to be really jenky. Probably because I was using a couple of If statments everytime. So I started the thread on a post delay of 500ms from the onUserInteraction. This gives enough time for the zoomLevel to update before the thread starts to run thus getting the correct zoomlevel without running a thread every 500ms.
public void onUserInteraction() {
handler.postDelayed(zoomChecker, zoomCheckingDelay);
}
Related
I'm making a launcher and I am stuck on making a long click listener for the widgets. I made a class that extends AppWidgetHost and another that extends AppWidgetHostView. They intercept the touch event and if it's action up it looks and sees if the action down lasted for 400L. It works ok unless there is no button on the widget. For example, the clock widget can not be long pressed.
Here is the implementation of the longClickListener on the host view:
hostView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
new AlertDialog.Builder(WidgetEdge.this)
.setTitle("Options")
.setMessage("Do you want to delete or resize widget?")
.setIcon(android.R.drawable.sym_def_app_icon)
.setNegativeButton("Delete", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int whichButton) {
removeWidget(hostView);
Toast.makeText(WidgetEdge.this, "Widget Deleted", Toast.LENGTH_SHORT).show();
}
})
.setPositiveButton("Resize", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialogInterface, int i) {
resizeView(hostView);
}
}).show();
return false;
}
});
Here is the AppWidgetHostView class:
public class LauncherAppWidgetHostView extends AppWidgetHostView{
private LayoutInflater mInflater;
WidgetEdge context;
private OnLongClickListener longClick;
private long down;
public LauncherAppWidgetHostView(Context context) {
super(context);
this.context = (WidgetEdge) context;
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#Override
public void setOnLongClickListener(OnLongClickListener l) {
this.longClick = l;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean trueOrFalse = false;
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
down = System.currentTimeMillis();
this.getParent().requestDisallowInterceptTouchEvent(true);
trueOrFalse = false;
break;
case MotionEvent.ACTION_UP:
boolean upVal = System.currentTimeMillis() - down > 400L;
if( upVal ) {
longClick.onLongClick(LauncherAppWidgetHostView.this);
trueOrFalse = true;
}
break;
}
return trueOrFalse;
}
#Override
protected View getErrorView() {
return mInflater.inflate(R.layout.appwidget_error, this, false);
}
}
Here is the AppWidgetHost:
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
class LauncherAppWidgetHost extends AppWidgetHost {
LauncherAppWidgetHost(Context context, int hostId) {
super(context, hostId);
}
#Override
protected AppWidgetHostView onCreateView(Context context, int appWidgetId,
AppWidgetProviderInfo appWidget) {
return new LauncherAppWidgetHostView(context);
}
#Override
public void stopListening() {
super.stopListening();
clearViews();
}
}
I have tried using the code from this link but when I tested on the clock widget it launches the onLongClickListener twice. Also when the widget is scrolled, without a long press, it would also launch the onLongClick. Thank you for any help.
--UPDATE--
I was using the debugger and found out that when using the clock widget the only event intercepted was the first ACTION_DOWN. After that it never picked up the ACTION_UP.
If the widget doesn't behave like a button (so when it can't be clicked) you need to do something more advanced to detect the long click.
You can have a look at https://github.com/willli666/Android-Trebuchet-Launcher-Standalone/blob/master/src/com/cyanogenmod/trebuchet/LauncherAppWidgetHostView.java
If the Apache licence works for your project, you can copy-paste the whole file, you just need to remove getErrorView() and the inflater and you're good to go.
The idea is to start a timeout when detecting the initial ACTION_DOWN event, and when the timeout triggers, if the view still has focus, then you can performLongClick().
It's much harder to accomplish than one would expect, but at least this works on all widgets, even those that can't be clicked.
I'm making a litle game in android studio where I want background music to play when the user is interacting with the application (and have it turned on in the settings). For this I used a service for playing backgroundmusic. As you can see below
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;
public class BackgroundMusicService extends Service {
MediaPlayer player;
public IBinder onBind(Intent arg0) {
return null;
}
#Override
public void onCreate() {
super.onCreate();
player = MediaPlayer.create(this, R.raw.backgroundmusic);
player.setLooping(true); // Set looping
}
public int onStartCommand(Intent intent, int flags, int startId) {
player.start();
return 1;
}
#Override
public void onDestroy() {
player.stop();
player.release();
}
#Override
public void onLowMemory() {
}
}
I want my backgroundmusic to play on all my activities, but stop when the user leaves the application, as it is impossible to detect a menu button press or a home button press in android. I tried to solve it like this.
import java.util.Timer;
import java.util.TimerTask;
public class LeaveAppDetector extends BackgroundMusicService{
private boolean Deactivated;
private int Time = 700;
private int wait = 0;
private Timer timer = new Timer();
public void Activate() {
Deactivated = false;
timer.schedule(new TimerTask() {
#Override
public void run() {
if (wait >= 1) {
if (!getDeactivate()) {
//app has been closed
StopPlaying();
StopTimer();
} else {StopTimer();} //app has not been closed
}
wait++;
}
}, Time);
}
public void Deactivate() { //call at the start of each activity
Deactivated = true;
}
private boolean getDeactivate() {
return Deactivated;
}
private void StopTimer() {
if (timer != null) {
timer.cancel();
timer.purge();
}
}
}
The idea is that when an activity starts, it calls Deactivate(); and when it closes, it calls Activate(); so that the value of Activated is updated after a short period of time. I added the following method to my BackgrondMusicService class in order to be able to turn it off remotely
public void StopPlaying() {
player.stop();
player.release();
}
Now the problem, it gives me the error that the reference to the mediaplayers in the method I just showed, is one referring to a null object, and I don't know why or why it won't work. Can someone help?
You need to use a Bound Service like this answer suggest:
https://stackoverflow.com/a/2660696/3742122
If you only want the service running while activities are using it, consider getting rid of startService(). Instead, use bindService() in the onStart() methods of the activities that need the service, and call unbindService() in their corresponding onStop() methods. You can use BIND_AUTO_CREATE to have the service be lazy-started when needed, and Android will automatically stop the service after all connections have been unbound.
I've been trying to implement an onStateChange listener and have looked across the internet for help, but nothing seems to solve my specific case. My issue is that I'm making a text based adventure, and I need to disallow clicks until the textbox has finished its "typewriter" style printing. Once it finishes, an arrow image should appear and you should now be able to click the textbox to advance the text. Unfortunately, since the text appearing character by character appears in the special TextView class, I'm not sure how to use an onStateChange with my active activity(ies), the special TextView class, and the often recommended third class I've seen in examples. Here is what I have so far:
I've seen many suggestions recommend a class like this:
public class TypewriterManager {
private TypewriterListener typewriterListener = null;
public interface TypewriterListener {
public void onStateChange(boolean state);
}
public void registerListener (TypewriterListener listener) {
typewriterListener = listener;
}
// -----------------------------
// the part that this class does
private boolean isTypewriterFinished = false;
public void doYourWork() {
// do things here
// at some point
isTypewriterFinished = false;
// now notify if someone is interested.
if (typewriterListener != null)
typewriterListener.onStateChange(isTypewriterFinished);
}
}
Now, I imagine I would somehow call the onStateChange in the runnable?
public class Typewriter extends TextView {
private CharSequence mText;
private int mIndex;
private long mDelay = 500; //Default 500ms delay
public Typewriter(Context context) {
super(context);
}
public Typewriter(Context context, AttributeSet attrs) {
super(context, attrs);
}
private Handler mHandler = new Handler();
private Runnable characterAdder = new Runnable() {
#Override
public void run() {
setText(mText.subSequence(0, mIndex++));
if(mIndex <= mText.length()) {
mHandler.postDelayed(characterAdder, mDelay);
}else{
//call listener?
}
}
};
}
However, I need to call this listener in my main activities where the Typewriter TextBox physically is, and that's where I'm stumped. Maybe it's the way my brain is used to Android, but I want something that looks like:
typewriter.setOnTypewriterFinishedListener(new View.OnTypewriterFinishedListener)
Any help or feedback would be appreciated.
I have an android app I am just experimenting things on and I cannot seem to figure out why my app force closes when I update a TextView via a while loop. When I comment out the updateText method it runs fine.
public class GameThread extends Thread {
Thread t;
private int i;
private boolean running;
private long sleepTime;
GameView gv;
public GameThread() {
t = new Thread(this);
t.start();
i = 0;
sleepTime = 1000;
}
public void initView(GameView v) {
this.gv = v;
}
public void setRunning(boolean b) {
this.running = b;
}
public boolean getRunning() {
return running;
}
public void run() {
while(running) {
i++;
update();
try {
t.sleep(sleepTime);
} catch(InterruptedException e) {
}
}
}
public void update() {
gv.setText(i); // when this is uncommented, it causes force close
Log.v("Semajhan", "i = " + i);
}
public class GameView extends LinearLayout {
public TextView tv;
public GameView(Context c) {
super(c);
this.setBackgroundColor(Color.WHITE);
tv = new TextView(c);
tv.setTextColor(Color.BLACK);
tv.setTextSize(20);
this.addView(tv);
}
public void setText(int i) {
tv.setText("i count: " + i);
}
public class Exp extends Activity {
GameThread t;
GameView v;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
v = new GameView(this);
setContentView(v);
t = new GameThread();
t.setRunning(true);
t.initView(v);
}
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (t.getRunning() == true) {
t.setRunning(false);
Log.v("Semajhan", "STOPPED");
} else {
t.setRunning(true);
Log.v("Semajhan", "RESTART");
}
}
return true;
}
protected void onDestroy() {
Log.v("Semajhan", "DESTROYING");
super.onDestroy();
}
protected void onStop() {
Log.v("Semajhan", "Stopping");
super.onStop();
}
I though i'd post the whole app since it is relatively small and so that I could get some help without confusion.
First, when you get a Force Close dialog, use adb logcat, DDMS, or the DDMS perspective in Eclipse to examine LogCat and look at the stack trace associated with your crash.
In this case, your exception will be something to the effect of "Cannot modify the user interface from a non-UI thread". You are attempting to call setText() from a background thread, which is not supported.
Using a GameThread makes sense if you are using 2D/3D graphics. It is not an appropriate pattern for widget-based applications. There are many, many, many, many examples that demonstrate how to create widget-based applications without the use of a GameThread.
You have to call it from the UI thread.
For more info check: Painless Threading .
If you decide to use a Handler, the easiest solution for you will be to:
Extend a View, override it's onDraw , in it draw the game objects, after you have calculated the game data for them first of course
The Handler: (in your Activity)
private Handler playHandler = new Handler() {
public void handleMessage(Message msg) {
gameView.postInvalidate(); // gameView is the View that you extended
}
};
The game thread has a simple
Message.obtain(playHandler).sendToTarget();
In 2 words, the View is responsible for the drawing (you can move the calculations in a separate class, and call it before the onDraw), the thread is responsible only for scheduled calls to the Handler, and the Handler is responsible only to tell the View to redraw itself.
You cannot update the UI of your app outside of the UI Thread, which is the 'main' thread you start in. In onCreate(Context) of you app, you are creating the game thread object, which is what is doing the updating of your UI.
You should use a Handler:
http://developer.android.com/reference/android/os/Handler.html
I just registered an OnLongClickListener on my my MapView on an Android app I'm currently writing. For some reason however the onLongClick event doesn't fire.
Here's what I've written so far:
public class FriendMapActivity extends MapActivity implements OnLongClickListener {
private static final int CENTER_MAP = Menu.FIRST;
private MapView mapView;
private MapController mapController;
//...
private boolean doCenterMap = true;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.friendmapview);
this.mapView = (MapView) findViewById(R.id.map_view);
this.mapController = mapView.getController();
mapView.setBuiltInZoomControls(true);
mapView.displayZoomControls(true);
mapView.setLongClickable(true);
mapView.setOnLongClickListener(new OnLongClickListener() {
public boolean onLongClick(View v) {
//NEVER FIRES!!
return false;
}
});
//...
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_3:
mapController.zoomIn();
break;
case KeyEvent.KEYCODE_1:
mapController.zoomOut();
break;
}
return super.onKeyDown(keyCode, event);
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int actionType = ev.getAction();
switch (actionType) {
case MotionEvent.ACTION_MOVE:
doCenterMap = false;
break;
}
return super.dispatchTouchEvent(ev);
}
...
}
May overlays which I'm adding cause the problem?? Any suggestions?
I ran into the same problem and there is a simple solution to your problem actually; it's because you're using the wrong type of listener.
You should use the OnMapLongClickListener() object from the OnMapLongClickListener interface.
Hopefully everything should work properly :)
Please tell me if it works.
I just ran into this problem. I tried the solution above, but it doesn't quite work 100% in that we want the long press action to fire, even if the user is still holding a finger down.
This is how I implemented a solution, using a handler and a delayed task -
As a side note, I used a similar type implementation, but in reverse, to hide/show zoom controls on touch/etc..
private Handler mHandler = new Handler();
private final Runnable mTask = new Runnable() {
#Override
public void run() {
// your code here
}
};
#Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// record the start time, start the timer
mEventStartTime = ev.getEventTime();
mHandler.postDelayed(mTask, LONG_PRESS_TIME);
} else if (ev.getAction() == MotionEvent.ACTION_UP) {
// record the end time, dont show if not long enough
mEventEndTime = ev.getEventTime();
if (mEventEndTime - mEventStartTime < LONG_PRESS_TIME) {
mHandler.removeCallbacks(mTask);
}
} else {
// moving, panning, etc .. up to you whether you want to
// count this as a long press - reset timing to start from now
mEventStartTime = ev.getEventTime();
mHandler.removeCallbacks(mTask);
mHandler.postDelayed(mTask, LONG_PRESS_TIME);
}
return super.onTouchEvent(ev);
}
In the mean time I found the "solution" (or workaround, call it as you like) by myself. The way I worked through this issue is by using a GestureDetector and forwarding all touch events to that object by implementing an according OnGestureListener interface.
I've posted some code on my blog if anyone is interested:
http://juristr.com/blog/2009/12/mapview-doesnt-fire-onlongclick-event/
Don't ask me why this didn't work by hooking up the OnLongClickListener directly on the MapView. If someone has an explanation let me know :)
UPDATE:
My previously suggested solution using a GestureDetector posed some drawbacks. So I updated the blog post on my site.
In WebView framework code performLongClick() is used to handle long press event, this is how Android copy Text Feature is implemented in Browser, that is why onLongClick is not been fired.