package carbon.gesture; import android.content.Context; import android.os.Handler; import android.view.MotionEvent; import java.util.ArrayList; import java.util.List; import carbon.R; public class GestureDetector { private static final int DEFAULT_PRESS_TIMEOUT = 100; private static final int DEFAULT_LONGPRESS_TIMEOUT = 200; private static final int DEFAULT_TAP_TIMEOUT = 300; private int pressTimeout = DEFAULT_PRESS_TIMEOUT; private long longPressTimeout = DEFAULT_LONGPRESS_TIMEOUT; private int tapTimeout = DEFAULT_TAP_TIMEOUT; private int moveEpsilon; private Handler handler = new Handler(); private Runnable pressHandler; private Runnable longPressHandler; private Runnable tapHandler; private float prevTouchY, startTouchX; private float prevTouchX, startTouchY; private float prevCenterX, prevCenterY, prevDist, prevRotation; private boolean pressed; private int clicks = 0; private boolean moving, transforming = false; public GestureDetector(Context context) { moveEpsilon = context.getResources().getDimensionPixelSize(R.dimen.carbon_moveEpsilon); } private List<OnGestureListener> listeners = new ArrayList<>(); public void addOnGestureListener(OnGestureListener listener) { if (listener == null) throw new NullPointerException("Listener cannot be null"); listeners.add(listener); } public void removeOnGestureListener(OnGestureListener listener) { if (listener == null) throw new NullPointerException("Listener cannot be null"); listeners.remove(listener); } public void clearOnGestureListeners() { listeners.clear(); } public boolean shouldInterceptEvents(MotionEvent event) { if (event.getPointerCount() > 1) return true; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: moving = false; pressed = true; startTouchX = event.getX(); startTouchY = event.getY(); break; case MotionEvent.ACTION_MOVE: if (pressed) { if (!moving) { float dx = event.getX() - startTouchX; float dy = event.getY() - startTouchY; if (Math.sqrt(dx * dx + dy * dy) > moveEpsilon) return true; } else { return true; } } break; } return false; } public boolean onTouchEvent(final MotionEvent motionEvent) { final MotionEvent event = MotionEvent.obtain(motionEvent); handler.removeCallbacks(longPressHandler); longPressHandler = null; if (motionEvent.getPointerCount() == 1) { transforming = false; handleSinglePointer(event); } else if (motionEvent.getPointerCount() == 2) { handleTwoPointers(event); } else { } prevTouchX = motionEvent.getX(); prevTouchY = motionEvent.getY(); return moving; } private void handleSinglePointer(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: moving = false; pressed = true; startTouchX = event.getX(); startTouchY = event.getY(); clicks++; handler.removeCallbacks(tapHandler); tapHandler = null; pressHandler = () -> firePressEvent(event); handler.postDelayed(pressHandler, pressTimeout); longPressHandler = () -> fireLongPressEvent(event); handler.removeCallbacks(longPressHandler); handler.postDelayed(longPressHandler, longPressTimeout); break; case MotionEvent.ACTION_UP: if (pressed && pressHandler != null) { handler.removeCallbacks(pressHandler); pressHandler.run(); } if (clicks > 0) { fireTapEvent(event, clicks); tapHandler = () -> { tapHandler = null; clicks = 0; }; handler.postDelayed(tapHandler, tapTimeout); } pressed = false; break; case MotionEvent.ACTION_MOVE: if (pressed) { if (!moving) { float dx = event.getX() - startTouchX; float dy = event.getY() - startTouchY; if (Math.sqrt(dx * dx + dy * dy) > moveEpsilon) { handler.removeCallbacks(tapHandler); tapHandler = null; handler.removeCallbacks(pressHandler); pressHandler = null; handler.removeCallbacks(longPressHandler); longPressHandler = null; moving = true; fireDragEvent(event, dx, dy); } } else { fireDragEvent(event, event.getX() - prevTouchX, event.getY() - prevTouchY); } } break; case MotionEvent.ACTION_CANCEL: pressed = false; moving = false; handler.removeCallbacks(pressHandler); pressHandler = null; handler.removeCallbacks(tapHandler); tapHandler = null; handler.removeCallbacks(longPressHandler); longPressHandler = null; clicks = 0; } } private void handleTwoPointers(MotionEvent event) { clicks = 0; switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_POINTER_2_DOWN: case MotionEvent.ACTION_MOVE: if (!transforming) { transforming = true; handler.removeCallbacks(pressHandler); pressHandler = null; handler.removeCallbacks(longPressHandler); longPressHandler = null; handler.removeCallbacks(tapHandler); tapHandler = null; prevRotation = (float) Math.atan2(event.getY(0) - event.getY(1), event.getX(0) - event.getX(1)); prevDist = (float) Math.sqrt(Math.pow(event.getX(0) - event.getX(1), 2) + Math.pow(event.getY(0) - event.getY(1), 2)); prevCenterX = (event.getX(0) + event.getX(1)) / 2; prevCenterY = (event.getY(0) + event.getY(1)) / 2; } else { float dist = (float) Math.sqrt(Math.pow(event.getX(0) - event.getX(1), 2) + Math.pow(event.getY(0) - event.getY(1), 2)); float cx = (event.getX(0) + event.getX(1)) / 2; float cy = (event.getY(0) + event.getY(1)) / 2; float dx = cx - prevCenterX; float dy = cy - prevCenterY; float rotation = (float) Math.atan2(event.getY(0) - event.getY(1), event.getX(0) - event.getX(1)); float scale = dist / prevDist; float rx = rotation - prevRotation; fireTransformEvent(event, cx, cy, dx, dy, rx, scale); prevCenterX = cx; prevCenterY = cy; prevRotation = rotation; prevDist = dist; } break; } } private void fireTransformEvent(MotionEvent motionEvent, float cx, float cy, float dx, float dy, float rx, float scale) { for (OnGestureListener listener : listeners) listener.onTransform(motionEvent, cx, cy, dx, dy, rx, scale); } private void fireDragEvent(MotionEvent motionEvent, float translationX, float translationY) { clicks = 0; for (OnGestureListener listener : listeners) listener.onDrag(motionEvent, translationX, translationY); } private void fireTapEvent(MotionEvent motionEvent, int clicks) { tapHandler = null; for (OnGestureListener listener : listeners) listener.onTap(motionEvent, clicks); } private void fireLongPressEvent(MotionEvent motionEvent) { clicks = 0; longPressHandler = null; for (OnGestureListener listener : listeners) listener.onLongPress(motionEvent); } private void firePressEvent(MotionEvent motionEvent) { pressHandler = null; for (OnGestureListener listener : listeners) listener.onPress(motionEvent); } public int getPressTimeout() { return pressTimeout; } public void setPressTimeout(int pressTimeout) { this.pressTimeout = pressTimeout; } public long getLongPressTimeout() { return longPressTimeout; } public void setLongPressTimeout(long longPressTimeout) { this.longPressTimeout = longPressTimeout; } public int getTapTimeout() { return tapTimeout; } /** * @param tapTimeout time between subsequent release events to be interpreted as another tap */ public void setTapTimeout(int tapTimeout) { this.tapTimeout = tapTimeout; } public int getMoveEpsilon() { return moveEpsilon; } /** * @param moveEpsilon distance over which move and drag events are reported */ public void setMoveEpsilon(int moveEpsilon) { this.moveEpsilon = moveEpsilon; } }