Improved gestures: parallel system and samples #253

This commit is contained in:
Emux 2016-11-25 09:28:33 +02:00
parent 71f7c45b21
commit ba93445259
15 changed files with 934 additions and 173 deletions

View File

@ -3,6 +3,7 @@
## New since 0.6.0 ## New since 0.6.0
- Mapsforge multiple map files [#208](https://github.com/mapsforge/vtm/issues/208) - Mapsforge multiple map files [#208](https://github.com/mapsforge/vtm/issues/208)
- Improved gestures implementation [#253](https://github.com/mapsforge/vtm/issues/253)
- Polygon label position enhancements [#80](https://github.com/mapsforge/vtm/issues/80) - Polygon label position enhancements [#80](https://github.com/mapsforge/vtm/issues/80)
- vtm-web modules update [#51](https://github.com/mapsforge/vtm/issues/51) - vtm-web modules update [#51](https://github.com/mapsforge/vtm/issues/51)
- Mapbox vector tiles [#57](https://github.com/mapsforge/vtm/issues/57) - Mapbox vector tiles [#57](https://github.com/mapsforge/vtm/issues/57)

View File

@ -59,6 +59,9 @@
<activity <activity
android:name=".MultiMapActivity" android:name=".MultiMapActivity"
android:configChanges="keyboardHidden|orientation|screenSize" /> android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".NewGesturesActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity <activity
android:name=".OsmJsonMapActivity" android:name=".OsmJsonMapActivity"
android:configChanges="keyboardHidden|orientation|screenSize" /> android:configChanges="keyboardHidden|orientation|screenSize" />

View File

@ -0,0 +1,33 @@
/*
* Copyright 2016 devemux86
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.oscim.android.test;
import org.oscim.map.Map;
public class NewGesturesActivity extends MarkerOverlayActivity {
public NewGesturesActivity() {
super();
Map.NEW_GESTURES = true;
}
@Override
protected void onDestroy() {
super.onDestroy();
// Revert gestures for other activities
Map.NEW_GESTURES = false;
}
}

View File

@ -53,6 +53,7 @@ public class Samples extends Activity {
linearLayout.addView(createButton(VectorLayerMapActivity.class)); linearLayout.addView(createButton(VectorLayerMapActivity.class));
linearLayout.addView(createButton(MultiMapActivity.class)); linearLayout.addView(createButton(MultiMapActivity.class));
linearLayout.addView(createButton(MapFragmentActivity.class)); linearLayout.addView(createButton(MapFragmentActivity.class));
linearLayout.addView(createButton(NewGesturesActivity.class));
} }
private Button createButton(final Class<?> clazz) { private Button createButton(final Class<?> clazz) {

View File

@ -1,5 +1,6 @@
/* /*
* Copyright 2012 Hannes Janetzek * Copyright 2012 Hannes Janetzek
* Copyright 2016 devemux86
* *
* This file is part of the OpenScienceMap project (http://www.opensciencemap.org). * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
* *
@ -21,11 +22,13 @@ import android.content.Context;
import android.opengl.GLSurfaceView; import android.opengl.GLSurfaceView;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.GestureDetector;
import org.oscim.android.canvas.AndroidGraphics; import org.oscim.android.canvas.AndroidGraphics;
import org.oscim.android.gl.AndroidGL; import org.oscim.android.gl.AndroidGL;
import org.oscim.android.gl.GlConfigChooser; import org.oscim.android.gl.GlConfigChooser;
import org.oscim.android.input.AndroidMotionEvent; import org.oscim.android.input.AndroidMotionEvent;
import org.oscim.android.input.GestureHandler;
import org.oscim.backend.CanvasAdapter; import org.oscim.backend.CanvasAdapter;
import org.oscim.backend.GLAdapter; import org.oscim.backend.GLAdapter;
import org.oscim.map.Map; import org.oscim.map.Map;
@ -51,6 +54,7 @@ public class MapView extends GLSurfaceView {
} }
protected final AndroidMap mMap; protected final AndroidMap mMap;
protected GestureDetector mGestureDetector;
protected final AndroidMotionEvent mMotionEvent; protected final AndroidMotionEvent mMotionEvent;
public MapView(Context context) { public MapView(Context context) {
@ -91,6 +95,12 @@ public class MapView extends GLSurfaceView {
mMap.clearMap(); mMap.clearMap();
mMap.updateMap(false); mMap.updateMap(false);
if (!Map.NEW_GESTURES) {
GestureHandler gestureHandler = new GestureHandler(mMap);
mGestureDetector = new GestureDetector(context, gestureHandler);
mGestureDetector.setOnDoubleTapListener(gestureHandler);
}
mMotionEvent = new AndroidMotionEvent(); mMotionEvent = new AndroidMotionEvent();
} }
@ -112,6 +122,11 @@ public class MapView extends GLSurfaceView {
if (!isClickable()) if (!isClickable())
return false; return false;
if (mGestureDetector != null) {
if (mGestureDetector.onTouchEvent(motionEvent))
return true;
}
mMap.input.fire(null, mMotionEvent.wrap(motionEvent)); mMap.input.fire(null, mMotionEvent.wrap(motionEvent));
mMotionEvent.recycle(); mMotionEvent.recycle();
return true; return true;

View File

@ -0,0 +1,97 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@ -85,11 +85,11 @@ public abstract class GdxMap implements ApplicationListener {
mMapRenderer.onSurfaceChanged(w, h); mMapRenderer.onSurfaceChanged(w, h);
InputMultiplexer mux = new InputMultiplexer(); InputMultiplexer mux = new InputMultiplexer();
mGestureDetector = new GestureDetector(new LayerHandler(mMap)); if (!Map.NEW_GESTURES) {
mux.addProcessor(mGestureDetector); mGestureDetector = new GestureDetector(new LayerHandler(mMap));
mux.addProcessor(mGestureDetector);
}
mux.addProcessor(new InputHandler(this)); mux.addProcessor(new InputHandler(this));
//mux.addProcessor(new GestureDetector(20, 0.5f, 2, 0.05f,
// new MapController(mMap)));
mux.addProcessor(new MotionHandler(mMap)); mux.addProcessor(new MotionHandler(mMap));
Gdx.input.setInputProcessor(mux); Gdx.input.setInputProcessor(mux);

View File

@ -22,7 +22,6 @@ import com.badlogic.gdx.InputProcessor;
import org.oscim.event.MotionEvent; import org.oscim.event.MotionEvent;
import org.oscim.map.Map; import org.oscim.map.Map;
import org.oscim.utils.ArrayUtils;
import java.util.Arrays; import java.util.Arrays;

View File

@ -0,0 +1,28 @@
/*
* Copyright 2016 devemux86
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.oscim.test;
import org.oscim.gdx.GdxMapApp;
import org.oscim.map.Map;
public class NewGesturesTest extends MarkerLayerTest {
public static void main(String[] args) {
Map.NEW_GESTURES = true;
GdxMapApp.init();
GdxMapApp.run(new NewGesturesTest());
}
}

View File

@ -18,14 +18,13 @@ import static org.mockito.Mockito.when;
public class MapEventLayerTest { public class MapEventLayerTest {
private MapEventLayer layer; private MapEventLayer layer;
private Map mockMap;
private ViewController mockViewport; private ViewController mockViewport;
private Animator mockAnimator; private Animator mockAnimator;
private ArgumentCaptor<Float> argumentCaptor; private ArgumentCaptor<Float> argumentCaptor;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
mockMap = Mockito.mock(Map.class); Map mockMap = Mockito.mock(Map.class);
mockViewport = Mockito.mock(ViewController.class); mockViewport = Mockito.mock(ViewController.class);
mockAnimator = Mockito.mock(Animator.class); mockAnimator = Mockito.mock(Animator.class);
layer = new MapEventLayer(mockMap); layer = new MapEventLayer(mockMap);
@ -94,12 +93,12 @@ public class MapEventLayerTest {
layer.onTouchEvent(new TestMotionEvent(MotionEvent.ACTION_UP, 1, 2)); layer.onTouchEvent(new TestMotionEvent(MotionEvent.ACTION_UP, 1, 2));
} }
class TestMotionEvent extends MotionEvent { private class TestMotionEvent extends MotionEvent {
final int action; final int action;
final float x; final float x;
final float y; final float y;
public TestMotionEvent(int action, float x, float y) { TestMotionEvent(int action, float x, float y) {
this.action = action; this.action = action;
this.x = x; this.x = x;
this.y = y; this.y = y;

View File

@ -34,7 +34,7 @@ public interface Gesture {
final class TripleTap implements Gesture { final class TripleTap implements Gesture {
} }
class TwoFingerTap implements Gesture { final class TwoFingerTap implements Gesture {
} }
Gesture PRESS = new Press(); Gesture PRESS = new Press();

View File

@ -0,0 +1,42 @@
/*
* Copyright 2016 devemux86
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.oscim.layers;
import org.oscim.map.Map;
public abstract class AbstractMapEventLayer extends Layer {
public AbstractMapEventLayer(Map map) {
super(map);
}
public abstract void enableRotation(boolean enable);
public abstract boolean rotationEnabled();
public abstract void enableTilt(boolean enable);
public abstract boolean tiltEnabled();
public abstract void enableMove(boolean enable);
public abstract boolean moveEnabled();
public abstract void enableZoom(boolean enable);
public abstract boolean zoomEnabled();
public abstract void setFixOnCenter(boolean enable);
}

View File

@ -22,15 +22,11 @@ import org.oscim.core.MapPosition;
import org.oscim.core.Tile; import org.oscim.core.Tile;
import org.oscim.event.Event; import org.oscim.event.Event;
import org.oscim.event.Gesture; import org.oscim.event.Gesture;
import org.oscim.event.GestureListener;
import org.oscim.event.MotionEvent; import org.oscim.event.MotionEvent;
import org.oscim.map.Map; import org.oscim.map.Map;
import org.oscim.map.Map.InputListener; import org.oscim.map.Map.InputListener;
import org.oscim.map.ViewController; 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.backend.CanvasAdapter.dpi;
import static org.oscim.utils.FastMath.withinSquaredDist; import static org.oscim.utils.FastMath.withinSquaredDist;
@ -41,9 +37,7 @@ import static org.oscim.utils.FastMath.withinSquaredDist;
* TODO rewrite using gesture primitives to build more complex gestures: * TODO rewrite using gesture primitives to build more complex gestures:
* maybe something similar to this https://github.com/ucbvislab/Proton * maybe something similar to this https://github.com/ucbvislab/Proton
*/ */
public class MapEventLayer extends Layer implements InputListener { public class MapEventLayer extends AbstractMapEventLayer implements InputListener, GestureListener {
private static final Logger log = LoggerFactory.getLogger(MapEventLayer.class);
private boolean mEnableRotate = true; private boolean mEnableRotate = true;
private boolean mEnableTilt = true; private boolean mEnableTilt = true;
@ -62,12 +56,8 @@ public class MapEventLayer extends Layer implements InputListener {
private boolean mDoTilt; private boolean mDoTilt;
private boolean mDown; private boolean mDown;
private boolean mDoubleTap;
private boolean mDragZoom; private boolean mDragZoom;
private boolean mTwoFingers;
private boolean mTwoFingersDone;
private int mTaps;
private long mStartDown;
private MotionEvent mLastTap;
private float mPrevX1; private float mPrevX1;
private float mPrevY1; private float mPrevY1;
@ -89,30 +79,18 @@ public class MapEventLayer extends Layer implements InputListener {
private static final float PINCH_ROTATE_THRESHOLD = 0.2f; private static final float PINCH_ROTATE_THRESHOLD = 0.2f;
private static final float PINCH_ROTATE_THRESHOLD2 = 0.5f; 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 * 100 ms since start of move to reduce fling scroll
*/ */
private static final long FLING_MIN_THRESHOLD = 100; private static final float FLING_MIN_THREHSHOLD = 100;
private static final long DOUBLE_TAP_THRESHOLD = 300;
private static final long LONG_PRESS_THRESHOLD = 500;
private final VelocityTracker mTracker; private final VelocityTracker mTracker;
private final Timer mTimer;
private TimerTask mTimerTask;
private final MapPosition mapPosition = new MapPosition(); private final MapPosition mapPosition = new MapPosition();
public MapEventLayer(Map map) { public MapEventLayer(Map map) {
super(map); super(map);
mTracker = new VelocityTracker(); mTracker = new VelocityTracker();
mTimer = new Timer();
}
@Override
public void onDetach() {
mTimer.cancel();
mTimer.purge();
} }
@Override @Override
@ -120,34 +98,42 @@ public class MapEventLayer extends Layer implements InputListener {
onTouchEvent(motionEvent); onTouchEvent(motionEvent);
} }
@Override
public void enableRotation(boolean enable) { public void enableRotation(boolean enable) {
mEnableRotate = enable; mEnableRotate = enable;
} }
@Override
public boolean rotationEnabled() { public boolean rotationEnabled() {
return mEnableRotate; return mEnableRotate;
} }
@Override
public void enableTilt(boolean enable) { public void enableTilt(boolean enable) {
mEnableTilt = enable; mEnableTilt = enable;
} }
@Override
public boolean tiltEnabled() { public boolean tiltEnabled() {
return mEnableTilt; return mEnableTilt;
} }
@Override
public void enableMove(boolean enable) { public void enableMove(boolean enable) {
mEnableMove = enable; mEnableMove = enable;
} }
@Override
public boolean moveEnabled() { public boolean moveEnabled() {
return mEnableMove; return mEnableMove;
} }
@Override
public void enableZoom(boolean enable) { public void enableZoom(boolean enable) {
mEnableScale = enable; mEnableScale = enable;
} }
@Override
public boolean zoomEnabled() { public boolean zoomEnabled() {
return mEnableScale; return mEnableScale;
} }
@ -155,62 +141,29 @@ public class MapEventLayer extends Layer implements InputListener {
/** /**
* When enabled zoom- and rotation-gestures will not move the viewport. * When enabled zoom- and rotation-gestures will not move the viewport.
*/ */
@Override
public void setFixOnCenter(boolean enable) { public void setFixOnCenter(boolean enable) {
mFixOnCenter = enable; mFixOnCenter = enable;
} }
boolean onTouchEvent(final MotionEvent e) { boolean onTouchEvent(MotionEvent e) {
int action = getAction(e); int action = getAction(e);
final long time = e.getTime();
if (action == MotionEvent.ACTION_DOWN) { if (action == MotionEvent.ACTION_DOWN) {
if (mTimerTask != null) { mMap.animator().cancel();
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; mStartMove = -1;
mDragZoom = false; mDoubleTap = false;
mTwoFingers = false; mDragZoom = 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); mPrevX1 = e.getX(0);
mPrevY1 = e.getY(0); mPrevY1 = e.getY(0);
mDown = true;
return true; return true;
} }
if (!mDown) { if (!(mDown || mDoubleTap)) {
/* no down event received */ /* no down event received */
return false; return false;
} }
@ -221,12 +174,17 @@ public class MapEventLayer extends Layer implements InputListener {
} }
if (action == MotionEvent.ACTION_UP) { if (action == MotionEvent.ACTION_UP) {
mDown = false; mDown = false;
if (mTimerTask != null) { if (mDoubleTap && !mDragZoom) {
mTimerTask.cancel(); float pivotX = 0, pivotY = 0;
mTimer.purge(); if (!mFixOnCenter) {
mTimerTask = null; pivotX = mPrevX1 - mMap.getWidth() / 2;
} pivotY = mPrevY1 - mMap.getHeight() / 2;
if (mStartMove > 0) { }
/* handle double tap zoom */
mMap.animator().animateZoom(300, 2, pivotX, pivotY);
} else if (mStartMove > 0) {
/* handle fling gesture */ /* handle fling gesture */
mTracker.update(e.getX(), e.getY(), e.getTime()); mTracker.update(e.getX(), e.getY(), e.getTime());
float vx = mTracker.getVelocityX(); float vx = mTracker.getVelocityX();
@ -234,86 +192,16 @@ public class MapEventLayer extends Layer implements InputListener {
/* reduce velocity for short moves */ /* reduce velocity for short moves */
float t = e.getTime() - mStartMove; float t = e.getTime() - mStartMove;
if (t < FLING_MIN_THRESHOLD) { if (t < FLING_MIN_THREHSHOLD) {
t = t / FLING_MIN_THRESHOLD; t = t / FLING_MIN_THREHSHOLD;
vy *= t * t; vy *= t * t;
vx *= t * t; vx *= t * t;
} }
doFling(vx, vy); 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; return true;
} }
if (action == MotionEvent.ACTION_CANCEL) { if (action == MotionEvent.ACTION_CANCEL) {
mTaps = 0;
return false; return false;
} }
if (action == MotionEvent.ACTION_POINTER_DOWN) { if (action == MotionEvent.ACTION_POINTER_DOWN) {
@ -322,12 +210,6 @@ public class MapEventLayer extends Layer implements InputListener {
return true; return true;
} }
if (action == MotionEvent.ACTION_POINTER_UP) { 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); updateMulti(e);
return true; return true;
} }
@ -355,7 +237,12 @@ public class MapEventLayer extends Layer implements InputListener {
mPrevY1 = y1; mPrevY1 = y1;
/* double-tap drag zoom */ /* double-tap drag zoom */
if (mTaps == 1) { if (mDoubleTap) {
/* just ignore first move event to set mPrevX/Y */
if (!mDown) {
mDown = true;
return;
}
if (!mDragZoom && !isMinimalMove(mx, my)) { if (!mDragZoom && !isMinimalMove(mx, my)) {
mPrevX1 -= mx; mPrevX1 -= mx;
mPrevY1 -= my; mPrevY1 -= my;
@ -418,7 +305,6 @@ public class MapEventLayer extends Layer implements InputListener {
mCanScale = false; mCanScale = false;
mCanRotate = false; mCanRotate = false;
mDoTilt = true; mDoTilt = true;
mTwoFingersDone = true;
} }
} }
} }
@ -445,7 +331,6 @@ public class MapEventLayer extends Layer implements InputListener {
/* start rotate, disable tilt */ /* start rotate, disable tilt */
mDoRotate = true; mDoRotate = true;
mCanTilt = false; mCanTilt = false;
mTwoFingersDone = true;
mAngle = rad; mAngle = rad;
} else if (!mDoScale) { } else if (!mDoScale) {
@ -465,7 +350,6 @@ public class MapEventLayer extends Layer implements InputListener {
mDoRotate = true; mDoRotate = true;
mCanRotate = true; mCanRotate = true;
mAngle = rad; mAngle = rad;
mTwoFingersDone = true;
} }
} }
@ -481,7 +365,6 @@ public class MapEventLayer extends Layer implements InputListener {
mCanTilt = false; mCanTilt = false;
mDoScale = true; mDoScale = true;
mTwoFingersDone = true;
} }
} }
if (mDoScale || mDoRotate) { if (mDoScale || mDoRotate) {
@ -530,8 +413,6 @@ public class MapEventLayer extends Layer implements InputListener {
mPrevY1 = e.getY(0); mPrevY1 = e.getY(0);
if (cnt == 2) { if (cnt == 2) {
mTwoFingers = true;
mDoScale = false; mDoScale = false;
mDoRotate = false; mDoRotate = false;
mDoTilt = false; mDoTilt = false;
@ -564,6 +445,15 @@ public class MapEventLayer extends Layer implements InputListener {
return true; return true;
} }
@Override
public boolean onGesture(Gesture g, MotionEvent e) {
if (g == Gesture.DOUBLE_TAP) {
mDoubleTap = true;
return true;
}
return false;
}
private static class VelocityTracker { private static class VelocityTracker {
/* sample window, 200ms */ /* sample window, 200ms */
private static final int MAX_MS = 200; private static final int MAX_MS = 200;

View File

@ -0,0 +1,643 @@
/*
* Copyright 2013 Hannes Janetzek
* Copyright 2016 devemux86
* Copyright 2016 Andrey Novikov
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.oscim.layers;
import org.oscim.core.MapPosition;
import org.oscim.core.Tile;
import org.oscim.event.Event;
import org.oscim.event.Gesture;
import org.oscim.event.MotionEvent;
import org.oscim.map.Map;
import org.oscim.map.Map.InputListener;
import org.oscim.map.ViewController;
import java.util.Timer;
import java.util.TimerTask;
import static org.oscim.backend.CanvasAdapter.dpi;
import static org.oscim.utils.FastMath.withinSquaredDist;
/**
* Changes Viewport by handling move, fling, scale, rotation and tilt gestures.
* <p/>
* TODO rewrite using gesture primitives to build more complex gestures:
* maybe something similar to this https://github.com/ucbvislab/Proton
*/
public class MapEventLayer2 extends AbstractMapEventLayer implements InputListener {
private boolean mEnableRotate = true;
private boolean mEnableTilt = true;
private boolean mEnableMove = true;
private boolean mEnableScale = true;
private boolean mFixOnCenter = false;
/* possible state transitions */
private boolean mCanScale;
private boolean mCanRotate;
private boolean mCanTilt;
/* current gesture state */
private boolean mDoRotate;
private boolean mDoScale;
private boolean mDoTilt;
private boolean mDown;
private boolean mDragZoom;
private boolean mTwoFingers;
private boolean mTwoFingersDone;
private int mTaps;
private long mStartDown;
private MotionEvent mLastTap;
private float mPrevX1;
private float mPrevY1;
private float mPrevX2;
private float mPrevY2;
private double mAngle;
private double mPrevPinchWidth;
private long mStartMove;
/**
* 2mm as minimal distance to start move: dpi / 25.4
*/
private static final float MIN_SLOP = 25.4f / 2;
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
*/
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 MapEventLayer2(Map map) {
super(map);
mTracker = new VelocityTracker();
mTimer = new Timer();
}
@Override
public void onDetach() {
mTimer.cancel();
mTimer.purge();
}
@Override
public void onInputEvent(Event e, MotionEvent motionEvent) {
onTouchEvent(motionEvent);
}
@Override
public void enableRotation(boolean enable) {
mEnableRotate = enable;
}
@Override
public boolean rotationEnabled() {
return mEnableRotate;
}
@Override
public void enableTilt(boolean enable) {
mEnableTilt = enable;
}
@Override
public boolean tiltEnabled() {
return mEnableTilt;
}
@Override
public void enableMove(boolean enable) {
mEnableMove = enable;
}
@Override
public boolean moveEnabled() {
return mEnableMove;
}
@Override
public void enableZoom(boolean enable) {
mEnableScale = enable;
}
@Override
public boolean zoomEnabled() {
return mEnableScale;
}
/**
* When enabled zoom- and rotation-gestures will not move the viewport.
*/
@Override
public void setFixOnCenter(boolean enable) {
mFixOnCenter = enable;
}
private boolean onTouchEvent(final MotionEvent e) {
int action = getAction(e);
final long time = e.getTime();
if (action == MotionEvent.ACTION_DOWN) {
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;
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);
return true;
}
if (!mDown) {
/* no down event received */
return false;
}
if (action == MotionEvent.ACTION_MOVE) {
onActionMove(e);
return true;
}
if (action == MotionEvent.ACTION_UP) {
mDown = false;
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();
float vy = mTracker.getVelocityY();
/* reduce velocity for short moves */
float t = e.getTime() - mStartMove;
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) {
mStartMove = -1;
updateMulti(e);
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;
}
return false;
}
private static int getAction(MotionEvent e) {
return e.getAction() & MotionEvent.ACTION_MASK;
}
private void onActionMove(MotionEvent e) {
ViewController mViewport = mMap.viewport();
float x1 = e.getX(0);
float y1 = e.getY(0);
float mx = x1 - mPrevX1;
float my = y1 - mPrevY1;
float width = mMap.getWidth();
float height = mMap.getHeight();
if (e.getPointerCount() < 2) {
mPrevX1 = x1;
mPrevY1 = y1;
/* double-tap drag zoom */
if (mTaps == 1) {
if (!mDragZoom && !isMinimalMove(mx, my)) {
mPrevX1 -= mx;
mPrevY1 -= my;
return;
}
// TODO limit scale properly
mDragZoom = true;
mViewport.scaleMap(1 + my / (height / 6), 0, 0);
mMap.updateMap(true);
mStartMove = -1;
return;
}
/* simple move */
if (!mEnableMove)
return;
if (mStartMove < 0) {
if (!isMinimalMove(mx, my)) {
mPrevX1 -= mx;
mPrevY1 -= my;
return;
}
mStartMove = e.getTime();
mTracker.start(x1, y1, mStartMove);
return;
}
mViewport.moveMap(mx, my);
mTracker.update(x1, y1, e.getTime());
mMap.updateMap(true);
if (mMap.viewport().getMapPosition(mapPosition))
mMap.events.fire(Map.MOVE_EVENT, mapPosition);
return;
}
mStartMove = -1;
float x2 = e.getX(1);
float y2 = e.getY(1);
float dx = (x1 - x2);
float dy = (y1 - y2);
double rotateBy = 0;
float scaleBy = 1;
float tiltBy = 0;
mx = ((x1 + x2) - (mPrevX1 + mPrevX2)) / 2;
my = ((y1 + y2) - (mPrevY1 + mPrevY2)) / 2;
if (mCanTilt) {
float slope = (dx == 0) ? 0 : dy / dx;
if (Math.abs(slope) < PINCH_TILT_SLOPE) {
if (mDoTilt) {
tiltBy = my / 5;
} else if (Math.abs(my) > (dpi / PINCH_TILT_THRESHOLD)) {
/* enter exclusive tilt mode */
mCanScale = false;
mCanRotate = false;
mDoTilt = true;
mTwoFingersDone = true;
}
}
}
double pinchWidth = Math.sqrt(dx * dx + dy * dy);
double deltaPinch = pinchWidth - mPrevPinchWidth;
if (mCanRotate) {
double rad = Math.atan2(dy, dx);
double r = rad - mAngle;
if (mDoRotate) {
double da = rad - mAngle;
if (Math.abs(da) > 0.0001) {
rotateBy = da;
mAngle = rad;
deltaPinch = 0;
}
} else {
r = Math.abs(r);
if (r > PINCH_ROTATE_THRESHOLD) {
/* start rotate, disable tilt */
mDoRotate = true;
mCanTilt = false;
mTwoFingersDone = true;
mAngle = rad;
} else if (!mDoScale) {
/* reduce pinch trigger by the amount of rotation */
deltaPinch *= 1 - (r / PINCH_ROTATE_THRESHOLD);
} else {
mPrevPinchWidth = pinchWidth;
}
}
} else if (mDoScale && mEnableRotate) {
/* re-enable rotation when higher threshold is reached */
double rad = Math.atan2(dy, dx);
double r = rad - mAngle;
if (r > PINCH_ROTATE_THRESHOLD2) {
/* start rotate again */
mDoRotate = true;
mCanRotate = true;
mAngle = rad;
mTwoFingersDone = true;
}
}
if (mCanScale || mDoRotate) {
if (!(mDoScale || mDoRotate)) {
/* enter exclusive scale mode */
if (Math.abs(deltaPinch) > (dpi / PINCH_ZOOM_THRESHOLD)) {
if (!mDoRotate) {
mPrevPinchWidth = pinchWidth;
mCanRotate = false;
}
mCanTilt = false;
mDoScale = true;
mTwoFingersDone = true;
}
}
if (mDoScale || mDoRotate) {
scaleBy = (float) (pinchWidth / mPrevPinchWidth);
mPrevPinchWidth = pinchWidth;
}
}
if (!(mDoRotate || mDoScale || mDoTilt))
return;
float pivotX = 0, pivotY = 0;
if (!mFixOnCenter) {
pivotX = (x2 + x1) / 2 - width / 2;
pivotY = (y2 + y1) / 2 - height / 2;
}
synchronized (mViewport) {
if (!mDoTilt) {
if (rotateBy != 0)
mViewport.rotateMap(rotateBy, pivotX, pivotY);
if (scaleBy != 1)
mViewport.scaleMap(scaleBy, pivotX, pivotY);
if (!mFixOnCenter)
mViewport.moveMap(mx, my);
} else {
if (tiltBy != 0 && mViewport.tiltMap(-tiltBy))
mViewport.moveMap(0, my / 2);
}
}
mPrevX1 = x1;
mPrevY1 = y1;
mPrevX2 = x2;
mPrevY2 = y2;
mMap.updateMap(true);
}
private void updateMulti(MotionEvent e) {
int cnt = e.getPointerCount();
mPrevX1 = e.getX(0);
mPrevY1 = e.getY(0);
if (cnt == 2) {
mTwoFingers = true;
mDoScale = false;
mDoRotate = false;
mDoTilt = false;
mCanScale = mEnableScale;
mCanRotate = mEnableRotate;
mCanTilt = mEnableTilt;
mPrevX2 = e.getX(1);
mPrevY2 = e.getY(1);
double dx = mPrevX1 - mPrevX2;
double dy = mPrevY1 - mPrevY2;
mAngle = Math.atan2(dy, dx);
mPrevPinchWidth = Math.sqrt(dx * dx + dy * dy);
}
}
private boolean isMinimalMove(float mx, float my) {
float minSlop = (dpi / MIN_SLOP);
return !withinSquaredDist(mx, my, minSlop * minSlop);
}
private boolean doFling(float velocityX, float velocityY) {
int w = Tile.SIZE * 5;
int h = Tile.SIZE * 5;
mMap.animator().animateFling(velocityX * 2, velocityY * 2,
-w, w, -h, h);
return true;
}
private static class VelocityTracker {
/* sample window, 200ms */
private static final int MAX_MS = 200;
private static final int SAMPLES = 32;
private float mLastX, mLastY;
private long mLastTime;
private int mNumSamples;
private int mIndex;
private float[] mMeanX = new float[SAMPLES];
private float[] mMeanY = new float[SAMPLES];
private int[] mMeanTime = new int[SAMPLES];
public void start(float x, float y, long time) {
mLastX = x;
mLastY = y;
mNumSamples = 0;
mIndex = SAMPLES;
mLastTime = time;
}
public void update(float x, float y, long time) {
if (time == mLastTime)
return;
if (--mIndex < 0)
mIndex = SAMPLES - 1;
mMeanX[mIndex] = x - mLastX;
mMeanY[mIndex] = y - mLastY;
mMeanTime[mIndex] = (int) (time - mLastTime);
mLastTime = time;
mLastX = x;
mLastY = y;
mNumSamples++;
}
private float getVelocity(float[] move) {
mNumSamples = Math.min(SAMPLES, mNumSamples);
double duration = 0;
double amount = 0;
for (int c = 0; c < mNumSamples; c++) {
int index = (mIndex + c) % SAMPLES;
float d = mMeanTime[index];
if (c > 0 && duration + d > MAX_MS)
break;
duration += d;
amount += move[index] * (d / duration);
}
if (duration == 0)
return 0;
return (float) ((amount * 1000) / duration);
}
float getVelocityY() {
return getVelocity(mMeanY);
}
float getVelocityX() {
return getVelocity(mMeanX);
}
}
}

View File

@ -27,8 +27,10 @@ import org.oscim.event.EventDispatcher;
import org.oscim.event.EventListener; import org.oscim.event.EventListener;
import org.oscim.event.Gesture; import org.oscim.event.Gesture;
import org.oscim.event.MotionEvent; import org.oscim.event.MotionEvent;
import org.oscim.layers.AbstractMapEventLayer;
import org.oscim.layers.Layer; import org.oscim.layers.Layer;
import org.oscim.layers.MapEventLayer; import org.oscim.layers.MapEventLayer;
import org.oscim.layers.MapEventLayer2;
import org.oscim.layers.tile.TileLayer; import org.oscim.layers.tile.TileLayer;
import org.oscim.layers.tile.vector.OsmTileLayer; import org.oscim.layers.tile.vector.OsmTileLayer;
import org.oscim.layers.tile.vector.VectorTileLayer; import org.oscim.layers.tile.vector.VectorTileLayer;
@ -45,7 +47,12 @@ import org.slf4j.LoggerFactory;
public abstract class Map implements TaskQueue { public abstract class Map implements TaskQueue {
static final Logger log = LoggerFactory.getLogger(Map.class); private static final Logger log = LoggerFactory.getLogger(Map.class);
/**
* If true the {@link MapEventLayer2} will be used instead of default {@link MapEventLayer}.
*/
public static boolean NEW_GESTURES = false;
/** /**
* Listener interface for map update notifications. * Listener interface for map update notifications.
@ -105,7 +112,7 @@ public abstract class Map implements TaskQueue {
protected final Animator mAnimator; protected final Animator mAnimator;
protected final MapPosition mMapPosition; protected final MapPosition mMapPosition;
protected final MapEventLayer mEventLayer; protected final AbstractMapEventLayer mEventLayer;
protected boolean mClearMap = true; protected boolean mClearMap = true;
@ -134,12 +141,15 @@ public abstract class Map implements TaskQueue {
mAsyncExecutor = new AsyncExecutor(4, this); mAsyncExecutor = new AsyncExecutor(4, this);
mMapPosition = new MapPosition(); mMapPosition = new MapPosition();
mEventLayer = new MapEventLayer(this); if (NEW_GESTURES)
mEventLayer = new MapEventLayer2(this);
else
mEventLayer = new MapEventLayer(this);
mLayers.add(0, mEventLayer); mLayers.add(0, mEventLayer);
} }
public MapEventLayer getEventLayer() { public AbstractMapEventLayer getEventLayer() {
return mEventLayer; return mEventLayer;
} }