diff --git a/vtm-android/src/org/oscim/android/MapView.java b/vtm-android/src/org/oscim/android/MapView.java index 76440f78..3c44954d 100644 --- a/vtm-android/src/org/oscim/android/MapView.java +++ b/vtm-android/src/org/oscim/android/MapView.java @@ -21,13 +21,11 @@ import android.content.Context; import android.opengl.GLSurfaceView; import android.util.AttributeSet; import android.util.DisplayMetrics; -import android.view.GestureDetector; import org.oscim.android.canvas.AndroidGraphics; import org.oscim.android.gl.AndroidGL; import org.oscim.android.gl.GlConfigChooser; import org.oscim.android.input.AndroidMotionEvent; -import org.oscim.android.input.GestureHandler; import org.oscim.backend.CanvasAdapter; import org.oscim.backend.GLAdapter; import org.oscim.map.Map; @@ -53,7 +51,6 @@ public class MapView extends GLSurfaceView { } protected final AndroidMap mMap; - protected final GestureDetector mGestureDetector; protected final AndroidMotionEvent mMotionEvent; public MapView(Context context) { @@ -94,10 +91,6 @@ public class MapView extends GLSurfaceView { mMap.clearMap(); mMap.updateMap(false); - GestureHandler gestureHandler = new GestureHandler(mMap); - mGestureDetector = new GestureDetector(context, gestureHandler); - mGestureDetector.setOnDoubleTapListener(gestureHandler); - mMotionEvent = new AndroidMotionEvent(); } @@ -116,14 +109,11 @@ public class MapView extends GLSurfaceView { @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(android.view.MotionEvent motionEvent) { - if (!isClickable()) return false; - if (mGestureDetector.onTouchEvent(motionEvent)) - return true; - mMap.input.fire(null, mMotionEvent.wrap(motionEvent)); + mMotionEvent.recycle(); return true; } diff --git a/vtm-android/src/org/oscim/android/input/AndroidMotionEvent.java b/vtm-android/src/org/oscim/android/input/AndroidMotionEvent.java index 06ff61b0..a68279d5 100644 --- a/vtm-android/src/org/oscim/android/input/AndroidMotionEvent.java +++ b/vtm-android/src/org/oscim/android/input/AndroidMotionEvent.java @@ -1,5 +1,6 @@ /* * Copyright 2013 Hannes Janetzek + * Copyright 2016 Andrey Novikov * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -20,10 +21,10 @@ import org.oscim.event.MotionEvent; public class AndroidMotionEvent extends MotionEvent { - android.view.MotionEvent mEvent; + private android.view.MotionEvent mEvent; public MotionEvent wrap(android.view.MotionEvent e) { - mEvent = e; + mEvent = android.view.MotionEvent.obtain(e); return this; } @@ -57,6 +58,16 @@ public class AndroidMotionEvent extends MotionEvent { return mEvent.getPointerCount(); } + @Override + public MotionEvent copy() { + return new AndroidMotionEvent().wrap(mEvent); + } + + @Override + public void recycle() { + mEvent.recycle(); + } + @Override public long getTime() { return mEvent.getEventTime(); diff --git a/vtm-android/src/org/oscim/android/input/GestureHandler.java b/vtm-android/src/org/oscim/android/input/GestureHandler.java deleted file mode 100644 index fc875a06..00000000 --- a/vtm-android/src/org/oscim/android/input/GestureHandler.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2013 Hannes Janetzek - * Copyright 2016 devemux86 - * - * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). - * - * This program is free software: you can redistribute it and/or modify it under the - * terms of the GNU Lesser General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with - * this program. If not, see . - */ -package org.oscim.android.input; - -import android.view.GestureDetector.OnDoubleTapListener; -import android.view.GestureDetector.OnGestureListener; -import android.view.MotionEvent; - -import org.oscim.event.Gesture; -import org.oscim.map.Map; - -public class GestureHandler implements OnGestureListener, OnDoubleTapListener { - private final AndroidMotionEvent mMotionEvent; - private final Map mMap; - - // Quick scale (double tap + swipe) - protected boolean quickScale; - - public GestureHandler(Map map) { - mMotionEvent = new AndroidMotionEvent(); - mMap = map; - } - - /* OnGestureListener */ - - @Override - public boolean onSingleTapUp(MotionEvent e) { - return false; - } - - @Override - public void onShowPress(MotionEvent e) { - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { - return false; - } - - @Override - public void onLongPress(MotionEvent e) { - // Quick scale (no long press) - if (quickScale) - return; - - mMap.handleGesture(Gesture.LONG_PRESS, mMotionEvent.wrap(e)); - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - return false; - } - - @Override - public boolean onDown(MotionEvent e) { - quickScale = false; - - return mMap.handleGesture(Gesture.PRESS, mMotionEvent.wrap(e)); - } - - /* OnDoubleTapListener */ - - @Override - public boolean onSingleTapConfirmed(MotionEvent e) { - return mMap.handleGesture(Gesture.TAP, mMotionEvent.wrap(e)); - } - - @Override - public boolean onDoubleTapEvent(MotionEvent e) { - int action = e.getActionMasked(); - - // Quick scale - quickScale = (action == MotionEvent.ACTION_MOVE); - - return false; - } - - @Override - public boolean onDoubleTap(MotionEvent e) { - return mMap.handleGesture(Gesture.DOUBLE_TAP, mMotionEvent.wrap(e)); - } -} diff --git a/vtm-gdx/src/org/oscim/gdx/GdxMotionEvent.java b/vtm-gdx/src/org/oscim/gdx/GdxMotionEvent.java index 4989ec55..55b63129 100644 --- a/vtm-gdx/src/org/oscim/gdx/GdxMotionEvent.java +++ b/vtm-gdx/src/org/oscim/gdx/GdxMotionEvent.java @@ -49,6 +49,15 @@ public class GdxMotionEvent extends MotionEvent { return 0; } + @Override + public MotionEvent copy() { + return new GdxMotionEvent(action, x, y, button); + } + + @Override + public void recycle() { + } + @Override public long getTime() { return 0; diff --git a/vtm-gdx/src/org/oscim/gdx/MotionHandler.java b/vtm-gdx/src/org/oscim/gdx/MotionHandler.java index 430aff09..d5e59191 100644 --- a/vtm-gdx/src/org/oscim/gdx/MotionHandler.java +++ b/vtm-gdx/src/org/oscim/gdx/MotionHandler.java @@ -22,6 +22,9 @@ import com.badlogic.gdx.InputProcessor; import org.oscim.event.MotionEvent; import org.oscim.map.Map; +import org.oscim.utils.ArrayUtils; + +import java.util.Arrays; public class MotionHandler extends MotionEvent implements InputProcessor { private final Map mMap; @@ -77,6 +80,25 @@ public class MotionHandler extends MotionEvent implements InputProcessor { return mPointerDown; } + @Override + public MotionEvent copy() { + MotionHandler handler = new MotionHandler(mMap); + handler.mPointerDown = mPointerDown; + handler.mDownTime = mDownTime; + handler.mType = mType; + handler.mPointer = mPointer; + handler.mCurX = mCurX; + handler.mCurY = mCurY; + handler.mPointerX = Arrays.copyOf(mPointerX, 10); + handler.mPointerY = Arrays.copyOf(mPointerY, 10); + handler.mTime = mTime; + return handler; + } + + @Override + public void recycle() { + } + @Override public long getTime() { return (long) (mTime / 1000000d); diff --git a/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java b/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java index 19015758..8d8e390c 100644 --- a/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java +++ b/vtm-tests/test/org/oscim/layers/MapEventLayerTest.java @@ -139,5 +139,14 @@ public class MapEventLayerTest { public int getPointerCount() { return 0; } + + @Override + public MotionEvent copy() { + return new TestMotionEvent(action, x, y); + } + + @Override + public void recycle() { + } } } diff --git a/vtm/src/org/oscim/event/Gesture.java b/vtm/src/org/oscim/event/Gesture.java index 7fbfb05e..96d6960f 100644 --- a/vtm/src/org/oscim/event/Gesture.java +++ b/vtm/src/org/oscim/event/Gesture.java @@ -1,5 +1,6 @@ /* * Copyright 2013 Hannes Janetzek + * Copyright 2016 Andrey Novikov * * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * @@ -18,20 +19,28 @@ package org.oscim.event; public interface Gesture { - static final class Press implements Gesture { + final class Press implements Gesture { } - static final class LongPress implements Gesture { + final class LongPress implements Gesture { } - static final class Tap implements Gesture { + final class Tap implements Gesture { } - static final class DoubleTap implements Gesture { + final class DoubleTap implements Gesture { } - public static Gesture PRESS = new Press(); - public static Gesture LONG_PRESS = new LongPress(); - public static Gesture TAP = new Tap(); - public static Gesture DOUBLE_TAP = new DoubleTap(); + final class TripleTap implements Gesture { + } + + class TwoFingerTap implements Gesture { + } + + Gesture PRESS = new Press(); + Gesture LONG_PRESS = new LongPress(); + Gesture TAP = new Tap(); + Gesture DOUBLE_TAP = new DoubleTap(); + Gesture TRIPLE_TAP = new TripleTap(); + Gesture TWO_FINGER_TAP = new TwoFingerTap(); } diff --git a/vtm/src/org/oscim/event/GestureDetector.java b/vtm/src/org/oscim/event/GestureDetector.java deleted file mode 100644 index c2ce39f8..00000000 --- a/vtm/src/org/oscim/event/GestureDetector.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2013 Hannes Janetzek - * - * This file is part of the OpenScienceMap project (http://www.opensciencemap.org). - * - * This program is free software: you can redistribute it and/or modify it under the - * terms of the GNU Lesser General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with - * this program. If not, see . - */ -package org.oscim.event; - -import org.oscim.map.Map; - -public class GestureDetector { - - private final Map mMap; - - public GestureDetector(Map map) { - mMap = map; - } - - public boolean onTouchEvent(MotionEvent e) { - if (e.getAction() == MotionEvent.ACTION_DOWN) { - return mMap.handleGesture(Gesture.PRESS, e); - } - - return false; - } -} diff --git a/vtm/src/org/oscim/event/MotionEvent.java b/vtm/src/org/oscim/event/MotionEvent.java index 89327068..68c6fc62 100644 --- a/vtm/src/org/oscim/event/MotionEvent.java +++ b/vtm/src/org/oscim/event/MotionEvent.java @@ -44,4 +44,7 @@ public abstract class MotionEvent { public abstract int getPointerCount(); + public abstract MotionEvent copy(); + + public abstract void recycle(); } diff --git a/vtm/src/org/oscim/layers/MapEventLayer.java b/vtm/src/org/oscim/layers/MapEventLayer.java index a0a06ba0..2d8230b2 100644 --- a/vtm/src/org/oscim/layers/MapEventLayer.java +++ b/vtm/src/org/oscim/layers/MapEventLayer.java @@ -22,7 +22,6 @@ import org.oscim.core.MapPosition; import org.oscim.core.Tile; import org.oscim.event.Event; import org.oscim.event.Gesture; -import org.oscim.event.GestureListener; import org.oscim.event.MotionEvent; import org.oscim.map.Map; import org.oscim.map.Map.InputListener; @@ -30,6 +29,9 @@ import org.oscim.map.ViewController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Timer; +import java.util.TimerTask; + import static org.oscim.backend.CanvasAdapter.dpi; import static org.oscim.utils.FastMath.withinSquaredDist; @@ -39,9 +41,9 @@ import static org.oscim.utils.FastMath.withinSquaredDist; * TODO rewrite using gesture primitives to build more complex gestures: * maybe something similar to this https://github.com/ucbvislab/Proton */ -public class MapEventLayer extends Layer implements InputListener, GestureListener { +public class MapEventLayer extends Layer implements InputListener { - static final Logger log = LoggerFactory.getLogger(MapEventLayer.class); + private static final Logger log = LoggerFactory.getLogger(MapEventLayer.class); private boolean mEnableRotate = true; private boolean mEnableTilt = true; @@ -60,8 +62,12 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen private boolean mDoTilt; private boolean mDown; - private boolean mDoubleTap; private boolean mDragZoom; + private boolean mTwoFingers; + private boolean mTwoFingersDone; + private int mTaps; + private long mStartDown; + private MotionEvent mLastTap; private float mPrevX1; private float mPrevY1; @@ -75,26 +81,38 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen /** * 2mm as minimal distance to start move: dpi / 25.4 */ - protected static final float MIN_SLOP = 25.4f / 2; + private static final float MIN_SLOP = 25.4f / 2; - protected static final float PINCH_ZOOM_THRESHOLD = MIN_SLOP / 2; - protected static final float PINCH_TILT_THRESHOLD = MIN_SLOP / 2; - protected static final float PINCH_TILT_SLOPE = 0.75f; - protected static final float PINCH_ROTATE_THRESHOLD = 0.2f; - protected static final float PINCH_ROTATE_THRESHOLD2 = 0.5f; + private static final float PINCH_ZOOM_THRESHOLD = MIN_SLOP / 2; + private static final float PINCH_TILT_THRESHOLD = MIN_SLOP / 2; + private static final float PINCH_TILT_SLOPE = 0.75f; + private static final float PINCH_ROTATE_THRESHOLD = 0.2f; + private static final float PINCH_ROTATE_THRESHOLD2 = 0.5f; + //TODO Should be initialized with platform specific defaults /** * 100 ms since start of move to reduce fling scroll */ - protected static final float FLING_MIN_THREHSHOLD = 100; + private static final long FLING_MIN_THRESHOLD = 100; + private static final long DOUBLE_TAP_THRESHOLD = 300; + private static final long LONG_PRESS_THRESHOLD = 500; private final VelocityTracker mTracker; + private final Timer mTimer; + private TimerTask mTimerTask; private final MapPosition mapPosition = new MapPosition(); public MapEventLayer(Map map) { super(map); mTracker = new VelocityTracker(); + mTimer = new Timer(); + } + + @Override + public void onDetach() { + mTimer.cancel(); + mTimer.purge(); } @Override @@ -141,24 +159,58 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen mFixOnCenter = enable; } - public boolean onTouchEvent(MotionEvent e) { - + boolean onTouchEvent(final MotionEvent e) { int action = getAction(e); + final long time = e.getTime(); if (action == MotionEvent.ACTION_DOWN) { - mMap.animator().cancel(); + if (mTimerTask != null) { + mTimerTask.cancel(); + mTimer.purge(); + mTimerTask = null; + } + mMap.handleGesture(Gesture.PRESS, e); + mDown = true; + mStartDown = time; + if (mTaps > 0) { + float mx = e.getX(0) - mLastTap.getX(); + float my = e.getY(0) - mLastTap.getY(); + if (isMinimalMove(mx, my)) { + mTaps = 0; + log.debug("tap {} {}", mLastTap.getX(), mLastTap.getY()); + mMap.handleGesture(Gesture.TAP, mLastTap); + } + } else { + mMap.animator().cancel(); - mStartMove = -1; - mDoubleTap = false; - mDragZoom = false; + mStartMove = -1; + mDragZoom = false; + mTwoFingers = false; + mTwoFingersDone = false; + + mTimerTask = new TimerTask() { + @Override + public void run() { + if (mTwoFingers || mStartMove != -1) + return; + mMap.post(new Runnable() { + @Override + public void run() { + log.debug("long press {} {}", e.getX(), e.getY()); + mMap.handleGesture(Gesture.LONG_PRESS, e); + } + }); + } + }; + mTimer.schedule(mTimerTask, LONG_PRESS_THRESHOLD); + } mPrevX1 = e.getX(0); mPrevY1 = e.getY(0); - mDown = true; return true; } - if (!(mDown || mDoubleTap)) { + if (!mDown) { /* no down event received */ return false; } @@ -169,17 +221,12 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen } if (action == MotionEvent.ACTION_UP) { mDown = false; - if (mDoubleTap && !mDragZoom) { - float pivotX = 0, pivotY = 0; - if (!mFixOnCenter) { - pivotX = mPrevX1 - mMap.getWidth() / 2; - pivotY = mPrevY1 - mMap.getHeight() / 2; - } - - /* handle double tap zoom */ - mMap.animator().animateZoom(300, 2, pivotX, pivotY); - - } else if (mStartMove > 0) { + if (mTimerTask != null) { + mTimerTask.cancel(); + mTimer.purge(); + mTimerTask = null; + } + if (mStartMove > 0) { /* handle fling gesture */ mTracker.update(e.getX(), e.getY(), e.getTime()); float vx = mTracker.getVelocityX(); @@ -187,16 +234,86 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen /* reduce velocity for short moves */ float t = e.getTime() - mStartMove; - if (t < FLING_MIN_THREHSHOLD) { - t = t / FLING_MIN_THREHSHOLD; + if (t < FLING_MIN_THRESHOLD) { + t = t / FLING_MIN_THRESHOLD; vy *= t * t; vx *= t * t; } doFling(vx, vy); } + + if (time - mStartDown > LONG_PRESS_THRESHOLD) { + log.debug(" not a tap"); + // this was not a tap + mTaps = 0; + return true; + } + + if (mTaps > 0) { + if ((time - mLastTap.getTime()) >= DOUBLE_TAP_THRESHOLD) { + mTaps = 1; + log.debug("tap {} {}", mLastTap.getX(), mLastTap.getY()); + mMap.handleGesture(Gesture.TAP, mLastTap); + } else { + mTaps += 1; + } + } else { + mTaps = 1; + } + + if (mLastTap != null) { + mLastTap.recycle(); + } + mLastTap = e.copy(); + + if (mTaps == 3) { + mTaps = 0; + log.debug("triple tap {} {}", e.getX(), e.getY()); + mMap.handleGesture(Gesture.TRIPLE_TAP, e); + } else if (mTaps == 2) { + mTimerTask = new TimerTask() { + @Override + public void run() { + mTaps = 0; + if (mDragZoom) + return; + mMap.post(new Runnable() { + @Override + public void run() { + log.debug("double tap {} {}", e.getX(), e.getY()); + if (!mMap.handleGesture(Gesture.DOUBLE_TAP, e)) { + /* handle double tap zoom */ + final float pivotX = mFixOnCenter ? 0 : mPrevX1 - mMap.getWidth() / 2; + final float pivotY = mFixOnCenter ? 0 : mPrevY1 - mMap.getHeight() / 2; + mMap.animator().animateZoom(300, 2, pivotX, pivotY); + } + } + }); + } + }; + mTimer.schedule(mTimerTask, DOUBLE_TAP_THRESHOLD); + } else { + mTimerTask = new TimerTask() { + @Override + public void run() { + mTaps = 0; + if (!mTwoFingers && mStartMove == -1) { + mMap.post(new Runnable() { + @Override + public void run() { + log.debug("tap {} {}", e.getX(), e.getY()); + mMap.handleGesture(Gesture.TAP, e); + } + }); + } + } + }; + mTimer.schedule(mTimerTask, DOUBLE_TAP_THRESHOLD); + } return true; } if (action == MotionEvent.ACTION_CANCEL) { + mTaps = 0; return false; } if (action == MotionEvent.ACTION_POINTER_DOWN) { @@ -205,6 +322,12 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen return true; } if (action == MotionEvent.ACTION_POINTER_UP) { + if (e.getPointerCount() == 2 && !mTwoFingersDone) { + log.debug("two finger tap"); + if (!mMap.handleGesture(Gesture.TWO_FINGER_TAP, e)) { + mMap.animator().animateZoom(300, 0.5, 0f, 0f); + } + } updateMulti(e); return true; } @@ -232,12 +355,7 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen mPrevY1 = y1; /* double-tap drag zoom */ - if (mDoubleTap) { - /* just ignore first move event to set mPrevX/Y */ - if (!mDown) { - mDown = true; - return; - } + if (mTaps == 1) { if (!mDragZoom && !isMinimalMove(mx, my)) { mPrevX1 -= mx; mPrevY1 -= my; @@ -300,6 +418,7 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen mCanScale = false; mCanRotate = false; mDoTilt = true; + mTwoFingersDone = true; } } } @@ -326,6 +445,7 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen /* start rotate, disable tilt */ mDoRotate = true; mCanTilt = false; + mTwoFingersDone = true; mAngle = rad; } else if (!mDoScale) { @@ -345,6 +465,7 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen mDoRotate = true; mCanRotate = true; mAngle = rad; + mTwoFingersDone = true; } } @@ -360,6 +481,7 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen mCanTilt = false; mDoScale = true; + mTwoFingersDone = true; } } if (mDoScale || mDoRotate) { @@ -408,6 +530,8 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen mPrevY1 = e.getY(0); if (cnt == 2) { + mTwoFingers = true; + mDoScale = false; mDoRotate = false; mDoTilt = false; @@ -440,16 +564,7 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen return true; } - @Override - public boolean onGesture(Gesture g, MotionEvent e) { - if (g == Gesture.DOUBLE_TAP) { - mDoubleTap = true; - return true; - } - return false; - } - - static class VelocityTracker { + private static class VelocityTracker { /* sample window, 200ms */ private static final int MAX_MS = 200; private static final int SAMPLES = 32; @@ -512,11 +627,11 @@ public class MapEventLayer extends Layer implements InputListener, GestureListen return (float) ((amount * 1000) / duration); } - public float getVelocityY() { + float getVelocityY() { return getVelocity(mMeanY); } - public float getVelocityX() { + float getVelocityX() { return getVelocity(mMeanX); } }