package de.blau.android; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Handler; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; /** * From: http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html * @author Adam Powell, modified by Andrew Gregory for Vespucci * @author Jan Schejbal added long-click detection */ public abstract class VersionedGestureDetector { private static final float DRAG_THRESHOLD = 20f; private static final long LONG_PRESS_DELAY = 500; OnGestureListener mListener; public abstract boolean onTouchEvent(View v, MotionEvent ev); public interface OnGestureListener { void onDown(View v, float x, float y); void onClick(View v, float x, float y); void onUp(View v, float x, float y); /** @return true if long click events are handled, false if they should be ignored */ boolean onLongClick(View v, float x, float y); void onDrag(View v, float x, float y, float dx, float dy); void onScale(View v, float scaleFactor, float prevSpan, float curSpan); boolean onDoubleTap(View v, float x, float y); } public static VersionedGestureDetector newInstance(Context context, OnGestureListener listener) { final int sdkVersion = Build.VERSION.SDK_INT; VersionedGestureDetector detector = null; if (sdkVersion < Build.VERSION_CODES.ECLAIR) { detector = new CupcakeDetector(); } else if (sdkVersion < Build.VERSION_CODES.FROYO) { detector = new EclairDetector(); } else { detector = new FroyoDetector(context); } detector.mListener = listener; return detector; } @TargetApi(3) private static class CupcakeDetector extends VersionedGestureDetector { float mFirstTouchX; float mFirstTouchY; float mLastTouchX; float mLastTouchY; boolean hasDragged; boolean hasScaled; /** true if a long press has occurred since the last ACTION_DOWN, i.e. the ACTION_UP may need to be ignored */ boolean hasLongPressed; LongPressTrigger longPressTrigger; float getActiveX(MotionEvent ev) { return ev.getX(); } float getActiveY(MotionEvent ev) { return ev.getY(); } boolean shouldDrag() { return true; } @Override public boolean onTouchEvent(View v, MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mFirstTouchX = mLastTouchX = getActiveX(ev); mFirstTouchY = mLastTouchY = getActiveY(ev); mListener.onDown(v, mFirstTouchX, mFirstTouchY); hasDragged = false; hasScaled = false; hasLongPressed = false; startLongPress(v, ev); break; case MotionEvent.ACTION_MOVE: { if (hasLongPressed) { break; } final float x = getActiveX(ev); final float y = getActiveY(ev); if (shouldDrag()) { if (Math.abs(x - mFirstTouchX) > DRAG_THRESHOLD || Math.abs(y - mFirstTouchY) > DRAG_THRESHOLD) { hasDragged = true; } if (hasDragged) { mListener.onDrag(v, x, y, x - mLastTouchX, y - mLastTouchY); } } mLastTouchX = x; mLastTouchY = y; if (hasDragged || hasScaled) { stopLongPress(); } else { // Update long press position if (longPressTrigger != null) { longPressTrigger.x = x; longPressTrigger.y = y; } } } break; case MotionEvent.ACTION_UP: { final float x = getActiveX(ev); final float y = getActiveY(ev); mListener.onUp(v, x, y); if (hasLongPressed) { break; } stopLongPress(); if (!hasDragged && !hasScaled) { mListener.onClick(v, x, y); } } break; } return true; } /** * Called when ACTION_DOWN is received. * Schedules a long-click check for execution. * * @param v the view from which the ACTION_DOWN onTouch event came * @param ev the onTouch event starting the (possible) long click */ private void startLongPress(View v, MotionEvent ev) { stopLongPress(); longPressTrigger = new LongPressTrigger(v, getActiveX(ev), getActiveY(ev)); Handler h = new Handler(); h.postDelayed(longPressTrigger, LONG_PRESS_DELAY); } /** * If a long-click check is scheduled, marks it as canceled. * Call this when the click turned out not to be a long-click * (e.g. finger released, or drag/scale detected) */ private void stopLongPress() { if (longPressTrigger != null) { longPressTrigger.cancel(); longPressTrigger = null; } } /** * Runnable checking for a long-press. * Will be scheduled for execution by startLongPress. * * If not cancelled, will notify the listener of the long-press event * and set hasLongPressed. * */ private class LongPressTrigger implements Runnable { private View v; private float x; private float y; private boolean canceled = false; LongPressTrigger(View v, float x, float y) { this.v = v; this.x = x; this.y = y; } @Override public void run() { if (canceled) return; if (mListener.onLongClick(v, x, y)) { hasLongPressed = true; } } /** * If this press turned out to be anything else than a long-press, * call this to disable this LongPressTrigger */ public void cancel() { canceled = true; } } } @TargetApi(5) private static class EclairDetector extends CupcakeDetector { private static final int INVALID_POINTER_ID = -1; private int mActivePointerId = INVALID_POINTER_ID; private int mActivePointerIndex = 0; @Override float getActiveX(MotionEvent ev) { return ev.getX(mActivePointerIndex); } @Override float getActiveY(MotionEvent ev) { return ev.getY(mActivePointerIndex); } @Override public boolean onTouchEvent(View v, MotionEvent ev) { final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: mActivePointerId = ev.getPointerId(0); break; case MotionEvent.ACTION_CANCEL: mActivePointerId = INVALID_POINTER_ID; break; case MotionEvent.ACTION_POINTER_UP: final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); mLastTouchX = ev.getX(newPointerIndex); mLastTouchY = ev.getY(newPointerIndex); } break; } mActivePointerIndex = ev.findPointerIndex(mActivePointerId); boolean ret = super.onTouchEvent(v, ev); if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) { mActivePointerId = INVALID_POINTER_ID; mActivePointerIndex = ev.findPointerIndex(mActivePointerId); } return ret; } } @TargetApi(8) private static class FroyoDetector extends EclairDetector { private ScaleGestureDetector mDetector; private GestureDetector mGestureDetector; private View v; public FroyoDetector(Context context) { mDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { mListener.onScale(v, detector.getScaleFactor(), detector.getPreviousSpan(), detector.getCurrentSpan()); hasScaled = true; return true; } }); mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { float x = e.getX(); float y = e.getY(); mListener.onDoubleTap(v, x, y); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { float x = e.getX(); float y = e.getY(); mListener.onClick(v, x, y); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { float x = e2.getX(); float y = e2.getY(); mListener.onDrag(v, x, y, -distanceX, -distanceY); return true; } @Override public void onLongPress(MotionEvent e) { float x = e.getX(); float y = e.getY(); mListener.onLongClick(v, x, y); } @Override public boolean onDown(MotionEvent e) { float x = e.getX(); float y = e.getY(); mFirstTouchX = mLastTouchX = x; mFirstTouchY = mLastTouchY = y; mListener.onDown(v, x, y); return true; } @Override public boolean onSingleTapUp(MotionEvent e) { float x = e.getX(); float y = e.getY(); mListener.onUp(v, x, y); return true; } }); } @Override boolean shouldDrag() { return !mDetector.isInProgress(); } @Override /** * This used to call through to the non-froyo versions. Replaced by calls to the respective android methods. */ public boolean onTouchEvent(View v, MotionEvent ev) { this.v = v; mDetector.onTouchEvent(ev); mGestureDetector.onTouchEvent(ev); return true; } } }