I've been trying to build a tap detector that can detect both double and tripe tap. After my efforts failed I searched a long time on the net to find something ready to use but no luck! It's strange that libraries for something like this are so scarce. Any help ??
You can try something like this.
Though I would generally recommend against using triple taps as a pattern as it is not something users are generally used to, so unless it's properly communicated to them, most might never know they can triple tap a view. Same goes for double taping actually on mobile devices, it's not always an intuitive way to interact in that environment.
view.setOnTouchListener(new View.OnTouchListener() {
Handler handler = new Handler();
int numberOfTaps = 0;
long lastTapTimeMs = 0;
long touchDownMs = 0;
#Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchDownMs = System.currentTimeMillis();
break;
case MotionEvent.ACTION_UP:
handler.removeCallbacksAndMessages(null);
if ((System.currentTimeMillis() - touchDownMs) > ViewConfiguration.getTapTimeout()) {
//it was not a tap
numberOfTaps = 0;
lastTapTimeMs = 0;
break;
}
if (numberOfTaps > 0
&& (System.currentTimeMillis() - lastTapTimeMs) < ViewConfiguration.getDoubleTapTimeout()) {
numberOfTaps += 1;
} else {
numberOfTaps = 1;
}
lastTapTimeMs = System.currentTimeMillis();
if (numberOfTaps == 3) {
Toast.makeText(getApplicationContext(), "triple", Toast.LENGTH_SHORT).show();
//handle triple tap
} else if (numberOfTaps == 2) {
handler.postDelayed(new Runnable() {
#Override
public void run() {
//handle double tap
Toast.makeText(getApplicationContext(), "double", Toast.LENGTH_SHORT).show();
}
}, ViewConfiguration.getDoubleTapTimeout());
}
}
return true;
}
});
Here is a Kotlin implementation that can detect an arbitrary number of taps, and respects the various timeout and slop parameters found in the ViewConfiguration class. I have tried to minimise heap allocations in the event handlers.
import android.os.Handler
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import kotlin.math.abs
/*
* Detects an arbitrary number of taps in rapid succession
*
* The passed callback will be called for each tap, with two parameters:
* - the number of taps detected in rapid succession so far
* - a boolean flag indicating whether this is last tap of the sequence
*/
class MultiTapDetector(view: View, callback: (Int, Boolean) -> Unit) {
private var numberOfTaps = 0
private val handler = Handler()
private val doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout().toLong()
private val tapTimeout = ViewConfiguration.getTapTimeout().toLong()
private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong()
private val viewConfig = ViewConfiguration.get(view.context)
private var downEvent = Event()
private var lastTapUpEvent = Event()
data class Event(var time: Long = 0, var x: Float = 0f, var y: Float = 0f) {
fun copyFrom(motionEvent: MotionEvent) {
time = motionEvent.eventTime
x = motionEvent.x
y = motionEvent.y
}
fun clear() {
time = 0
}
}
init {
view.setOnTouchListener { v, event ->
when(event.action) {
MotionEvent.ACTION_DOWN -> {
if(event.pointerCount == 1) {
downEvent.copyFrom(event)
} else {
downEvent.clear()
}
}
MotionEvent.ACTION_MOVE -> {
// If a move greater than the allowed slop happens before timeout, then this is a scroll and not a tap
if(event.eventTime - event.downTime < tapTimeout
&& abs(event.x - downEvent.x) > viewConfig.scaledTouchSlop
&& abs(event.y - downEvent.y) > viewConfig.scaledTouchSlop) {
downEvent.clear()
}
}
MotionEvent.ACTION_UP -> {
val downEvent = this.downEvent
val lastTapUpEvent = this.lastTapUpEvent
if(downEvent.time > 0 && event.eventTime - event.downTime < longPressTimeout) {
// We have a tap
if(lastTapUpEvent.time > 0
&& event.eventTime - lastTapUpEvent.time < doubleTapTimeout
&& abs(event.x - lastTapUpEvent.x) < viewConfig.scaledDoubleTapSlop
&& abs(event.y - lastTapUpEvent.y) < viewConfig.scaledDoubleTapSlop) {
// Double tap
numberOfTaps++
} else {
numberOfTaps = 1
}
this.lastTapUpEvent.copyFrom(event)
// Send event
val taps = numberOfTaps
handler.postDelayed({
// When this callback runs, we know if it is the final tap of a sequence
// if the number of taps has not changed
callback(taps, taps == numberOfTaps)
}, doubleTapTimeout)
}
}
}
true
}
}
}
I developed an advanced version of the Iorgu solition that suits better my needs:
public abstract class OnTouchMultipleTapListener implements View.OnTouchListener {
Handler handler = new Handler();
private boolean manageInActionDown;
private float tapTimeoutMultiplier;
private int numberOfTaps = 0;
private long lastTapTimeMs = 0;
private long touchDownMs = 0;
public OnTouchMultipleTapListener() {
this(false, 1);
}
public OnTouchMultipleTapListener(boolean manageInActionDown, float tapTimeoutMultiplier) {
this.manageInActionDown = manageInActionDown;
this.tapTimeoutMultiplier = tapTimeoutMultiplier;
}
/**
*
* #param e
* #param numberOfTaps
*/
public abstract void onMultipleTapEvent(MotionEvent e, int numberOfTaps);
#Override
public final boolean onTouch(View v, final MotionEvent event) {
if (manageInActionDown) {
onTouchDownManagement(v, event);
} else {
onTouchUpManagement(v, event);
}
return true;
}
private void onTouchDownManagement(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchDownMs = System.currentTimeMillis();
handler.removeCallbacksAndMessages(null);
if (numberOfTaps > 0 && (System.currentTimeMillis() - lastTapTimeMs) < ViewConfiguration.getTapTimeout() * tapTimeoutMultiplier) {
numberOfTaps += 1;
} else {
numberOfTaps = 1;
}
lastTapTimeMs = System.currentTimeMillis();
if (numberOfTaps > 0) {
final MotionEvent finalMotionEvent = MotionEvent.obtain(event); // to avoid side effects
handler.postDelayed(new Runnable() {
#Override
public void run() {
onMultipleTapEvent(finalMotionEvent, numberOfTaps);
}
}, (long) (ViewConfiguration.getDoubleTapTimeout() * tapTimeoutMultiplier));
}
break;
case MotionEvent.ACTION_UP:
break;
}
}
private void onTouchUpManagement(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchDownMs = System.currentTimeMillis();
break;
case MotionEvent.ACTION_UP:
handler.removeCallbacksAndMessages(null);
if ((System.currentTimeMillis() - touchDownMs) > ViewConfiguration.getTapTimeout()) {
numberOfTaps = 0;
lastTapTimeMs = 0;
break;
}
if (numberOfTaps > 0 && (System.currentTimeMillis() - lastTapTimeMs) < ViewConfiguration.getDoubleTapTimeout()) {
numberOfTaps += 1;
} else {
numberOfTaps = 1;
}
lastTapTimeMs = System.currentTimeMillis();
if (numberOfTaps > 0) {
final MotionEvent finalMotionEvent = MotionEvent.obtain(event); // to avoid side effects
handler.postDelayed(new Runnable() {
#Override
public void run() {
onMultipleTapEvent(finalMotionEvent, numberOfTaps);
}
}, ViewConfiguration.getDoubleTapTimeout());
}
}
}
}
Use the view listener to detect first tap on the view object,then see how to manage twice back pressed to exit an activity on stackoverflow.com (use a handler post delay).
Clicking the back button twice to exit an activity
Related
I tried by creating custom class for mapview but it is not working. Can any one suggest me an approach. How to disable scroll when user tries to zoom. Scroll should be enable when zoom is not performed.
I Have solved the problem using below code.
Create variables in Class
private ScaleGestureDetector gestureDetector;
private long lastZoomTime = 0;
private float lastSpan = -1;
private long firstClick=0;
private long lastClick;
GoogleMap mMap;
In OnMapReady method add below code:
mMap = googleMap;
gestureDetector = new ScaleGestureDetector(getApplicationContext(), new ScaleGestureDetector.OnScaleGestureListener() {
#Override
public boolean onScale(ScaleGestureDetector detector) {
Log.i("onScale ", " onScale ");
if (lastSpan == -1) {
lastSpan = detector.getCurrentSpan();
}
if (detector.getEventTime() - lastZoomTime >= 50) {
lastZoomTime = detector.getEventTime();
mMap.animateCamera(CameraUpdateFactory.zoomBy(getZoomValue(detector.getCurrentSpan(), lastSpan)), 50, null);
lastSpan = detector.getCurrentSpan();
}
return false;
}
#Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
lastSpan = -1;
return true;
}
#Override
public void onScaleEnd(ScaleGestureDetector detector) {
lastSpan = -1;
}
});
Add Below Methods in class
private float getZoomValue(float currentSpan, float lastSpan) {
Log.i("ongetZoomValue ", " getZoomValue ");
double value = (Math.log(currentSpan / lastSpan) / Math.log(1.55d));
return (float) value;
}
public boolean dispatchTouchEvent(MotionEvent ev) {
//Log.i("on dispatchTouchEvent ", " dispatchTouchEvent ");
switch (ev.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_POINTER_DOWN:
fingers = fingers + 1;
break;
case MotionEvent.ACTION_POINTER_UP:
fingers = fingers - 1;
break;
case MotionEvent.ACTION_UP:
fingers = 0;
break;
case MotionEvent.ACTION_DOWN:
if (firstClick != 0 && System.currentTimeMillis() - firstClick > 300) {
Log.i("on count=0", " count=0 ");
count = 0;
}
count++;
if (count == 1) {
Log.i("on count ", " count=1 ");
firstClick = System.currentTimeMillis();
} else if (count == 2) {
Log.i("on count ", " count=2 ");
lastClick = System.currentTimeMillis();
if (lastClick - firstClick < 300) {
CameraUpdate getzoom = CameraUpdateFactory.zoomIn();
mMap.animateCamera(getzoom, 400, null);
Log.i("on double click ", " event ");
}
}
fingers=1;
break;
}
if (fingers >1) {
//Log.i("2fingersaction ", "2fingersaction ");
mMap.getUiSettings().setScrollGesturesEnabled(false);
//mMap.getUiSettings().setRotateGesturesEnabled(false); // disableScrolling()
}
else if (fingers < 1 )
{
//Log.i("onefinger ", "onefinger ");
mMap.getUiSettings().setScrollGesturesEnabled(true);
// enableScrolling();
}
if (fingers > 1) {
//Log.i("double if condition ", "doubleifcondition ");
return gestureDetector.onTouchEvent(ev);
} else
return super.dispatchTouchEvent(ev);
}
In my Android game I have implemented a custom code in the onTouchEvent method of a (custom) SurfaceView to emulate a ScrollView. I already tried an actual ScrollView, but due to performance and lack of customizability I preferred to override onTouchEvent myself.
It works perfectly, but I cannot properly emulate the inertia effect typical of scrollviews.
What I did right now is this:
int beginRawY = 0;
int beginScrollValue = 0;
Integer firstPointerId = null;
[...]
private boolean onTouchEvent(MotionEvent event) {
int index = event.getActionIndex();
int action = event.getActionMasked();
int pointerId = event.getPointerId(index);
int rawY = (int) event.getRawY();
// This is meant to deal with multitouch: scroll only if I'm using "the same finger".
boolean rightPointer = firstPointerId != null && firstPointerId == pointerId;
switch (action) {
case MotionEvent.ACTION_DOWN:
initVelocityTracker(event);
if(firstPointerId == null) {
// Save initial scrollValue and touch position
beginRawY = rawY;
beginScrollValue = scrollValue;
firstPointerId = pointerId;
return true;
}
case MotionEvent.ACTION_MOVE:
if(rightPointer) {
updateVelocity(event);
// Updates scroll value to new scroll value
scrollValue = beginScrollValue -rawY + beginRawY;
return true;
}
case MotionEvent.ACTION_UP:
if(rightPointer) {
//Start the Dissipator runnable, passing to it the speed of the player's touch (number of pixels in 30 milliseconds)
dissipator.setVelocity(updateVelocity(event).pixelY());
dissipator.run();
firstPointerId = null;
return true;
}
}
return false;
}
private void initVelocityTracker(MotionEvent event) {
if(mVelocityTracker == null) {
// Retrieve a new VelocityTracker object to watch the velocity of a motion.
mVelocityTracker = VelocityTracker.obtain();
}
else {
// Reset the velocity tracker back to its initial state.
mVelocityTracker.clear();
}
// Add a user's movement to the tracker.
mVelocityTracker.addMovement(event);
}
private PixelDot updateVelocity(MotionEvent event) {
int pointerId = event.getPointerId(event.getActionIndex());
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(30);
return new PixelDot(
VelocityTrackerCompat.getXVelocity(mVelocityTracker, pointerId),
VelocityTrackerCompat.getYVelocity(mVelocityTracker, pointerId));
}
[...]
private class DissipatorRunnable implements Runnable {
private float velocity = 0;
public void setVelocity(float velocity) {
this.velocity = velocity;
}
#Override
public void run() {
// Simply linear descreasing of the scroll speed
float vValue = velocity;
if(velocity > 0) {
while (vValue > 0 && scrollValue > 0) {
scrollValue = scrollValue - vValue;
vValue -= 5;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
while (vValue < 0 && scrollValue < height) {
scrollValue = scrollValue - vValue;
vValue += 5;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
Code is simplier than it looks:
I use a ScrollValue to represent "how much the player scrolled". It is the number of pixels.
On Touch Down a save th eposition of the finger and the actual scrollvalue. I initialize a VelocityTracker too.
On Touch Move I update the velocity tracker and the scrollvalue.
On Touch Up I start a custom Runnable called Dissipator:
Every tot milliseconds I reduce (or increment) scrollvalue by the last tracked velocity, and then reduce the velocity value.
This kinda works, but the effect doesn't look like a scrollview with proper inertia, inertia is instead silly and too strong.
What should I do to emulate standard scrollviews inertia?
I am creating an application. If the custom View goes off the screen, a method is called. Here is my code for the custom View.
public class CustomView extends View {
private boolean bubbleOver;
private static final int BITMAP_SIZE = 64;
private static final int REFRESH_RATE = 40;
private final Paint mPainter = new Paint();
private ScheduledFuture<?> mMoverFuture;
private int mScaledBitmapWidth;
private Bitmap mScaledBitmap;
// location, speed and direction of the bubble
private float mXPos, mYPos, mDx, mDy, mRadius, mRadiusSquared;
private long mRotate, mDRotate;
CustomView (Context context, float x, float y) {
super (context);
// Create a new random number generator to randomize size, rotation, speed and direction
Random r = new Random ();
// Creates the bubble bitmap for this BubbleView
createScaledBitmap (r);
// Radius of the Bitmap
mRadius = mScaledBitmapWidth / 2;
mRadiusSquared = mRadius * mRadius;
// Adjust position to center the bubble under user's finger
mXPos = x - mRadius;
mYPos = y - mRadius;
// Set the BubbleView's speed and direction
setSpeedAndDirection(r);
// Set the BubbleView's rotation
setRotation(r);
mPainter.setAntiAlias(true);
}
private void setRotation(Random r) {
if (speedMode == RANDOM) {
// TODO - set rotation in range [1..3]
mDRotate = r.nextInt (3) + 1;
} else {
mDRotate = 0;
}
}
private void setSpeedAndDirection(Random r) {
// Used by test cases
switch (speedMode) {
case SINGLE:
mDx = 20;
mDy = 20;
break;
case STILL:
// No speed
mDx = 0;
mDy = 0;
break;
default:
// Limit movement speed in the x and y direction to [-3..3] pixels per movement.
mDx = r.nextFloat() * 6.0f - 3.0f;
mDy = r.nextFloat() * 6.0f - 3.0f;
}
}
private void createScaledBitmap (Random r) {
if (speedMode != RANDOM) {
mScaledBitmapWidth = BITMAP_SIZE * 3;
} else {
mScaledBitmapWidth = (r.nextInt(3) + 1) * BITMAP_SIZE;
}
mScaledBitmap = Bitmap.createScaledBitmap(mBitmap, mScaledBitmapWidth, mScaledBitmapWidth, true);
}
// Start moving the BubbleView & updating the display
private void start () {
// Creates a WorkerThread
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
// Execute the run() in Worker Thread every REFRESH_RATE milliseconds. Save reference to this job in mMoverFuture
mMoverFuture = executor.scheduleWithFixedDelay(new Runnable() {
#Override
public void run() {
if (moveWhileOnScreen()) {
stop (false);
} else {
BubbleView.this.postInvalidate();
}
}
}, 0, REFRESH_RATE, TimeUnit.MILLISECONDS);
}
// Returns true if the BubbleView intersects position (x,y)
private synchronized boolean intersects(float x, float y) {
// TODO - Return true if the BubbleView intersects position (x,y)
if ((mXPos <= x) && (x <= mXPos + mScaledBitmapWidth) && (mYPos <= y) && (y <= mYPos + mScaledBitmapWidth)) {
return true;
}
return false;
}
// Cancel the Bubble's movement. Remove Bubble from mFrame.
// Play pop sound if the BubbleView was popped
private void stop (final boolean wasPopped) {
if (null != mMoverFuture && !mMoverFuture.isDone()) {
mMoverFuture.cancel(true);
}
// This work will be performed on the UI Thread
mFrame.post(new Runnable() {
#Override
public void run() {
mFrame.removeView(BubbleView.this);
if (wasPopped) {
mSoundPool.play(mSoundID, mStreamVolume, mStreamVolume, 1, 0, 1f);
}
}
});
}
// Change the Bubble's speed and direction
private synchronized void deflect(float velocityX, float velocityY) {
mDx = velocityX / REFRESH_RATE;
mDy = velocityY / REFRESH_RATE;
}
// Draw the Bubble at its current location
#Override
protected synchronized void onDraw(Canvas canvas) {
canvas.save();
mRotate += mDRotate;
canvas.rotate(mRotate, mXPos + (mScaledBitmapWidth / 2), mYPos + (mScaledBitmapWidth / 2));
canvas.drawBitmap(mScaledBitmap, mXPos, mYPos, mPainter);
canvas.restore();
}
// Returns true if the BubbleView is still on the screen after the move operation
private synchronized boolean moveWhileOnScreen() {
mXPos += mDx;
mYPos += mDy;
return isOutOfView();
}
// Return true if the BubbleView is off the screen after the move operation
private boolean isOutOfView() {
if ((mXPos + mDisplayWidth < 0) || (mYPos + mDisplayHeight < 0) || (mXPos> mDisplayWidth) || (mYPos > mDisplayHeight)) {
loseGame();
return true;
}
return false;
}
}
When the View goes off the screen, loseGame() is not called. Only if another CustomView is created, loseGame() is called.
You have wrong sum to check, try with mScaledBitmapWidth not the mDisplayWidth.
if ((mXPos + mScaledBitmapWidth < 0)
|| (mYPos + mScaledBitmapWidth < 0)
|| (mXPos> mDisplayWidth)
|| (mYPos > mDisplayHeight)
{
loseGame();
return true;
}
I am making a game where there there are frogs hopping around the screen. Once a frog is touched, I change the game image to what I have set as "deadFrog" and its movement stops. I have them all created under and array-list, and I am unsure as to how to only make changes to the individual frog. Right now, if one frog is tapped, all of the frogs stop moving and change to deadFrog. Hopefully you can help me fix the tactical nuke of a tap ;) *If you need any more information, just comment and I'll be sure to provide it!
Edit Is there not a way to access a single element in the blocks arraylist? I've tried doing blocks(1) for example but that's invalid.
Here is where the frogs are declared:
public void init() {
blocks = new ArrayList<Block>();
for (int i = 0; i < 5; i++) {
Block b = new Block(i * 200, MainActivity.GAME_HEIGHT - 95,
BLOCK_WIDTH, BLOCK_HEIGHT);
blocks.add(b);
tapped = false;
}
}
They are rendered with this:
private void renderFrogs(Painter g) {
if (!tapped) {
for (int i = 0; i < blocks.size(); i++) {
Block b = blocks.get(i);
if (b.isVisible()) {
Assets.runAnim.render(g, (int) b.getX(), (int) b.getY());
}
}
}
if (tapped) {
for (int i = 0; i < blocks.size(); i++) {
Block b = blocks.get(i);
if (b.isVisible()) {
g.drawImage(Assets.deadfrog, (int) b.getX(), (int) b.getY());
}
}
}
}
And this is the onTouchListener:
public boolean onTouch(MotionEvent e, int scaledX, int scaledY) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
recentTouchY = scaledY;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
for (int i = 0; i < blocks.size(); i++) {
Block b = blocks.get(i);
if ((scaledY >= b.getY() - BLOCK_HEIGHT || scaledY <= b.getY()) && (scaledX >= b.getX() || scaledX <= b.getX() + BLOCK_WIDTH)) {
tapped = true;
}
}
}
return true;
}
Of course all frogs turn dead, you are supposed to keep "tapped" variable for each frog, your tapped variable is for all frogs at once.
Declare a class
public class Frog extends View{
public Drawable liveFrog;
public Drawable deadFrog;
public boolean isDead;
public Point location;
public int width;
public int height;
public Frog(Context context, int x, int y,int width,int height){
super(context);
this.isDead = false;
this.location = new Point(x,y);
this.width = width;
this.height = height;
}
public void onDraw(Canvas c){
super.onDraw(c);
if(!isDead){
//draw live frog at x,y
}else {
//draw dead frog at x,y
}
}
}
then you array is supposed to contain frogs
public void init(Context context) {
blocks = new ArrayList<Frog>();
for (int i = 0; i < 5; i++) {
Frog b = new Frog(context,i * 200, MainActivity.GAME_HEIGHT - 95,
BLOCK_WIDTH, BLOCK_HEIGHT);
blocks.add(b);
}
}
private void renderFrogs() {
for(Frog f : blocks){
//cause redraw
f.invalidate();
}
}
here comes the fun part, when you tap the frog
public boolean onTouch(MotionEvent e, int scaledX, int scaledY) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
recentTouchY = scaledY;
} else if (e.getAction() == MotionEvent.ACTION_UP) {
for (int i = 0; i < blocks.size(); i++) {
Frog frog = blocks.get(i);
if ((scaledY >= frog.getY() - BLOCK_HEIGHT || scaledY <= frog.getY()) && (scaledX >= frog.getX() || scaledX <= frog.getX() + BLOCK_WIDTH)) {
frog.isDead = true;
//cause one frog redraw
frog.invalidate();
//if the event was handled, stop here (unless you can have multiple frogs one on top of the other ?
return true;
}
}
}
//if the event was not handled, let it bubble up
return false;
}
From what I can see, your "tapped" boolean is not a property of EACH frog. It is declared once, and when triggered, per your for loop, will make every frog dead (obviously, since that's what you're experiencing).
Once "tapped" is true, your for loop is going through every block and assigning a dead frog to it.
I think you need to create a Frog class, and store instances of those in your ArrayList. A variable of the new frog class will be "touched", and when that is triggered you will do something to that specific instance only.
I have a custom View with bitmaps on it that the user can drag about.
I want to make it so when they long click one of them I can pop up a context menu with options such as reset position etc.
In the custom View I add my OnLongClickListener:
this.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
// show context menu..
return true;
}
});
And override onTouchEvent to look something like this:
public boolean onTouchEvent(MotionEvent event) {
handleDrag(event);
super.onTouchEvent(event);
return true;
}
The handleDrag function finds what object is been pressed, and handles updating it's position.
My problem is that when I start to drag an image the OnLongClickListener fires also. I'm not sure the best way around this.
I've tried adding a threshold to handleDrag to return false if user touches down but doesn't attempt to drag, but I'm finding it still difficult to get the correct handler fired.
Can anyone suggest a way to skip the OnLongClickListener while dragging?
I think I have this solved through tweaking my threshold approach.
First, I changed my onTouchEvent to look like this:
public boolean onTouchEvent(MotionEvent event) {
mMultiTouchController.handleDrag(event);
return super.onTouchEvent(event);
}
They now both fire, so I then changed my OnLongClickListener to the following:
this.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
if (!mMultiTouchController.has_moved) {
// Pop menu and done...
return false;
}
return true;
}
});
(mMultiTouchController is the class containing all my gesture detection code).
The key here is within this class, I added the bool 'has_moved'. When I go to start a drag I then compute the delta:
float diffX = Math.abs(mCurrPtX - mPrevPt.getX());
float diffY = Math.abs(mCurrPtY - mPrevPt.getY());
if (diffX < threshold && diffY < threshold) {
has_moved = false;
return;
}
Now when the onLongClick fires I know whether to take action or not.
The final piece was to set:
setHapticFeedbackEnabled(false);
in my View so that the user doesn't get a vibrate every time the longClick fires but no action is taken. I plan to do the vibration manually as a next step.
This seems to be ok so far, hope that helps anyone who has come across a similar situation as this one.
I would stop using the onLongClickListener and just implement your own, which is pretty easy to do. Then you have the control you need to keep them from interfering with each other.
The following code implements the following gestures: drag, tap, double tap, long click, and pinch.
static final short NONE = 0;
static final short DRAG = 1;
static final short ZOOM = 2;
static final short TAP = 3;
static final short DOUBLE_TAP = 4;
static final short POST_GESTURE = 5;
short mode = NONE;
static final float MIN_PINCH_DISTANCE = 30f;
static final float MIN_DRAG_DISTANCE = 5f;
static final float DOUBLE_TAP_MAX_DISTANCE = 30f;
static final long MAX_DOUBLE_TAP_MS = 1000;
static final long LONG_PRESS_THRESHOLD_MS = 2000;
public class Vector2d {
public float x;
public float y;
public Vector2d() {
x = 0f;
y = 0f;
}
public void set(float newX, float newY) {
x = newX;
y = newY;
}
public Vector2d avgVector(Vector2d remote) {
Vector2d mid = new Vector2d();
mid.set((remote.x + x)/2, (remote.y + y)/2);
return mid;
}
public float length() {
return (float) Math.sqrt(x * x + y * y);
}
public float distance(Vector2d remote) {
float deltaX = remote.x - x;
float deltaY = remote.y - y;
return (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
}
private Vector2d finger1 = new Vector2d();
private Vector2d finger2 = new Vector2d();
private Vector2d pinchStartDistance = new Vector2d();
private Vector2d pinchMidPoint;
private Vector2d fingerStartPoint = new Vector2d();
private long gestureStartTime;
private Marker selectedMarker;
#Override
public boolean onTouch(View v, MotionEvent event) {
// Dump touch event to log
dumpEvent(event);
// Handle touch events here...
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
finger1.set(event.getX(), event.getY());
if (mode == TAP) {
if (finger1.distance(fingerStartPoint) < DOUBLE_TAP_MAX_DISTANCE) {
mode = DOUBLE_TAP;
} else {
mode = NONE;
gestureStartTime = SystemClock.uptimeMillis();
}
} else {
gestureStartTime = SystemClock.uptimeMillis();
}
fingerStartPoint.set(event.getX(), event.getY());
break;
case MotionEvent.ACTION_POINTER_DOWN:
finger2.set(event.getX(1), event.getY(1));
pinchStartDistance.set(Math.abs(finger1.x - finger2.x), Math.abs(finger1.y - finger2.y));
Log.d(TAG, String.format("pinch start distance = %f, %f", pinchStartDistance.x, pinchStartDistance.y));
if (pinchStartDistance.length() > MIN_PINCH_DISTANCE) {
if (pinchStartDistance.x < MIN_PINCH_DISTANCE) {
pinchStartDistance.x = MIN_PINCH_DISTANCE;
}
if (pinchStartDistance.y < MIN_PINCH_DISTANCE) {
pinchStartDistance.y = MIN_PINCH_DISTANCE;
}
pinchMidPoint = finger1.avgVector(finger2);
mode = ZOOM;
Log.d(TAG, "mode=ZOOM" );
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if (mode == ZOOM) {
Vector2d pinchEndDistance = new Vector2d();
pinchEndDistance.set(Math.abs(finger1.x - finger2.x), Math.abs(finger1.y - finger2.y));
if (pinchEndDistance.x < MIN_PINCH_DISTANCE) {
pinchEndDistance.x = MIN_PINCH_DISTANCE;
}
if (pinchEndDistance.y < MIN_PINCH_DISTANCE) {
pinchEndDistance.y = MIN_PINCH_DISTANCE;
}
Log.d(TAG, String.format("pinch end distance = %f, %f", pinchEndDistance.x, pinchEndDistance.y));
zoom(pinchMidPoint, pinchStartDistance.x/pinchEndDistance.x, pinchStartDistance.y/pinchEndDistance.y);
// Set mode to "POST_GESTURE" so that when the other finger lifts the handler won't think it was a
// tap or something.
mode = POST_GESTURE;
} else if (mode == NONE) {
// The finger wasn't moved enough for it to be considered a "drag", so it is either a tap
// or a "long press", depending on how long it was down.
if ((SystemClock.uptimeMillis() - gestureStartTime) < LONG_PRESS_THRESHOLD_MS) {
Log.d(TAG, "mode=TAP");
mode = TAP;
selectedMarker = checkForMarker(finger1);
if (selectedMarker != null) {
Log.d(TAG, "Selected marker, mode=NONE");
mode = NONE;
((Activity) parent).showDialog(ResultsActivity.DIALOG_MARKER_ID);
}
}
else {
Log.d(TAG, "mode=LONG_PRESS");
addMarker(finger1);
requestRender();
}
} else if (mode == DOUBLE_TAP && (SystemClock.uptimeMillis() - gestureStartTime) < MAX_DOUBLE_TAP_MS) {
// The finger was again not moved enough for it to be considered a "drag", so it is
// a double-tap. Change the center point and zoom in.
Log.d(TAG, "mode=DOUBLE_TAP");
zoom(fingerStartPoint, 0.5f, 0.5f);
mode = NONE;
} else {
mode = NONE;
Log.d(TAG, "mode=NONE" );
}
break;
case MotionEvent.ACTION_MOVE:
if (mode == NONE || mode == TAP || mode == DOUBLE_TAP) {
finger1.set(event.getX(), event.getY());
if (finger1.distance(fingerStartPoint) > MIN_DRAG_DISTANCE) {
Log.d(TAG, "mode=DRAG" );
mode = DRAG;
scroll(fingerStartPoint.x - finger1.x, fingerStartPoint.y - finger1.y);
}
}
else if (mode == DRAG) {
scroll(finger1.x - event.getX(), finger1.y - event.getY());
finger1.set(event.getX(), event.getY());
}
else if (mode == ZOOM) {
for (int i=0; i<event.getPointerCount(); i++) {
if (event.getPointerId(i) == 0) {
finger1.set(event.getX(i), event.getY(i));
}
else if (event.getPointerId(i) == 1) {
finger2.set(event.getX(i), event.getY(i));
}
else {
Log.w(TAG, String.format("Unknown motion event pointer id: %d", event.getPointerId(i)));
}
}
}
break;
}
return true;
}
/** Show an event in the LogCat view, for debugging */
private void dumpEvent(MotionEvent event) {
String names[] = { "DOWN" , "UP" , "MOVE" , "CANCEL" , "OUTSIDE" ,
"POINTER_DOWN" , "POINTER_UP" , "7?" , "8?" , "9?" };
StringBuilder sb = new StringBuilder();
int action = event.getAction();
int actionCode = action & MotionEvent.ACTION_MASK;
sb.append("event ACTION_" ).append(names[actionCode]);
if (actionCode == MotionEvent.ACTION_POINTER_DOWN
|| actionCode == MotionEvent.ACTION_POINTER_UP) {
sb.append("(pid " ).append(
action >> MotionEvent.ACTION_POINTER_ID_SHIFT);
sb.append(")" );
}
sb.append("[" );
for (int i = 0; i < event.getPointerCount(); i++) {
sb.append("#" ).append(i);
sb.append("(pid " ).append(event.getPointerId(i));
sb.append(")=" ).append((int) event.getX(i));
sb.append("," ).append((int) event.getY(i));
if (i + 1 < event.getPointerCount())
sb.append(";" );
}
sb.append("]" );
Log.d(TAG, sb.toString());
}
//This code is to handle the gestures detection
final Handler handler = new Handler();
private Runnable mLongPressRunnable;
detector = new GestureDetector(this, new MyGestureDectector());
view.setOnTouchListener(new OnTouchListener() {
#SuppressLint("ClickableViewAccessibility")
#SuppressWarnings("deprecation")
#Override
public boolean onTouch(View v, MotionEvent event) {
detector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_DOWN) {
handler.postDelayed(mLongPressRunnable, 1000);
}
if ((event.getAction() == MotionEvent.ACTION_MOVE)
|| (event.getAction() == MotionEvent.ACTION_UP)) {
handler.removeCallbacks(mLongPressRunnable);
}
}
return true;
}
});
mLongPressRunnable = new Runnable() {
public void run() {
Toast.makeText(MainActivity.this, "long", Toast.LENGTH_SHORT)
.show();
}
};
class MyGestureDectector implements GestureDetector.OnDoubleTapListener,
OnGestureListener {
//Implement all the methods
}