All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jogamp.newt.event.DoubleTapScrollGesture Maven / Gradle / Ivy

/**
 * Copyright 2013 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */
package com.jogamp.newt.event;

import jogamp.newt.Debug;

/**
 * 2 pointer scroll/rotate gesture handler processing {@link MouseEvent}s
 * while producing {@link MouseEvent#EVENT_MOUSE_WHEEL_MOVED} events if gesture is completed.
 * 

* Criteria related to parameters: *

 *    - doubleTapSlop (scaled in pixels):
 *       - Max 2 finger distance to start 'scroll' mode
 *       - Max. distance diff of current 2-pointer middle and initiated 2-pointer middle.
 *
 *    - touchSlop (scaled in pixels):
 *       - Min. movement w/ 2 pointer within ScaledDoubleTapSlop starting 'scroll' mode
 *
 *    - Avoid computation if not within gesture, especially for MOVE/DRAG
 *
 *    - Only allow gesture to start with PRESS
 *
 *    - Leave gesture completely with RELEASE of both/all fingers, or dist-diff exceeds doubleTapSlop
 *
 *    - Tolerate temporary lift 1 of 2 pointer
 *
 *     - Always validate pointer-id
 * 
*

* Implementation uses a n-state to get detect gesture: *

*

* * * * * * *
from to action
NONE 1PRESS 1-pointer-pressed
1PRESS 2PRESS_T 2-pointer-pressed within doubleTapSlope
2PRESS_T SCROLL 2-pointer dragged, dist-diff within doubleTapSlop and scrollLen >= scrollSlop
2PRESS_C SCROLL 2-pointer dragged, dist-diff within doubleTapSlop
SCROLL SCROLL 2-pointer dragged, dist-diff within doubleTapSlop
* State ST_2PRESS_C merely exist to pick up gesture after one pointer has been lost temporarily. *

*

* {@link #isWithinGesture()} returns gestureState >= 2PRESS_C *

*/ public class DoubleTapScrollGesture implements GestureHandler { /** Scroll threshold in pixels (fallback), defaults to 16 pixels. Can be overriden by integer property newt.event.scroll_slop_pixel.*/ public static final int SCROLL_SLOP_PIXEL; /** Two pointer 'double tap' slop in pixels (fallback), defaults to 104 pixels. Can be overriden by integer property newt.event.double_tap_slop_pixel.*/ public static final int DOUBLE_TAP_SLOP_PIXEL; /** Scroll threshold in millimeter, defaults to 3 mm. Can be overriden by integer property newt.event.scroll_slop_mm.*/ public static final float SCROLL_SLOP_MM; /** Two pointer 'double tap' slop in millimeter, defaults to 20 mm. Can be overriden by integer property newt.event.double_tap_slop_mm.*/ public static final float DOUBLE_TAP_SLOP_MM; static { Debug.initSingleton(); SCROLL_SLOP_PIXEL = Debug.getIntProperty("newt.event.scroll_slop_pixel", true, 16); DOUBLE_TAP_SLOP_PIXEL = Debug.getIntProperty("newt.event.double_tap_slop_pixel", true, 104); SCROLL_SLOP_MM = Debug.getIntProperty("newt.event.scroll_slop_mm", true, 3); DOUBLE_TAP_SLOP_MM = Debug.getIntProperty("newt.event.double_tap_slop_mm", true, 20); } private static final int ST_NONE = 0; private static final int ST_1PRESS = 1; private static final int ST_2PRESS_T = 2; private static final int ST_2PRESS_C = 3; private static final int ST_SCROLL = 4; private final int scrollSlop, scrollSlopSquare, doubleTapSlop, doubleTapSlopSquare; private final float[] scrollDistance = new float[] { 0f, 0f }; private int[] pIds = new int[] { -1, -1 }; /** See class docu */ private int gestureState; private int sqStartDist; private int lastX, lastY; private int pointerDownCount; private MouseEvent hitGestureEvent; private static final int getSquareDistance(float x1, float y1, float x2, float y2) { final int deltaX = (int) x1 - (int) x2; final int deltaY = (int) y1 - (int) y2; return deltaX * deltaX + deltaY * deltaY; } private int gesturePointers(final MouseEvent e, final int excludeIndex) { int j = 0; for(int i=e.getPointerCount()-1; i>=0; i--) { if( excludeIndex != i ) { final int id = e.getPointerId(i); if( pIds[0] == id || pIds[1] == id ) { j++; } } } return j; } /** * scaledScrollSlop < scaledDoubleTapSlop * @param scaledScrollSlop Distance a pointer can wander before we think the user is scrolling in pixels. * @param scaledDoubleTapSlop Distance in pixels between the first touch and second touch to still be considered a double tap. */ public DoubleTapScrollGesture(int scaledScrollSlop, int scaledDoubleTapSlop) { scrollSlop = scaledScrollSlop; scrollSlopSquare = scaledScrollSlop * scaledScrollSlop; doubleTapSlop = scaledDoubleTapSlop; doubleTapSlopSquare = scaledDoubleTapSlop * scaledDoubleTapSlop; pointerDownCount = 0; clear(true); if(DEBUG) { System.err.println("DoubleTapScroll scrollSlop (scaled) "+scrollSlop); System.err.println("DoubleTapScroll doubleTapSlop (scaled) "+doubleTapSlop); } } @Override public String toString() { return "DoubleTapScroll[state "+gestureState+", in "+isWithinGesture()+", has "+(null!=hitGestureEvent)+", pc "+pointerDownCount+"]"; } @Override public void clear(boolean clearStarted) { scrollDistance[0] = 0f; scrollDistance[1] = 0f; hitGestureEvent = null; if( clearStarted ) { gestureState = ST_NONE; sqStartDist = 0; pIds[0] = -1; pIds[1] = -1; lastX = 0; lastY = 0; } } @Override public boolean isWithinGesture() { return ST_2PRESS_C <= gestureState; } @Override public boolean hasGesture() { return null != hitGestureEvent; } @Override public InputEvent getGestureEvent() { if( null != hitGestureEvent ) { final MouseEvent ge = hitGestureEvent; int modifiers = ge.getModifiers(); final float[] rotationXYZ = ge.getRotation(); rotationXYZ[0] = scrollDistance[0] / scrollSlop; rotationXYZ[1] = scrollDistance[1] / scrollSlop; if( rotationXYZ[0]*rotationXYZ[0] > rotationXYZ[1]*rotationXYZ[1] ) { // Horizontal scroll -> SHIFT modifiers |= com.jogamp.newt.event.InputEvent.SHIFT_MASK; } return new MouseEvent(MouseEvent.EVENT_MOUSE_WHEEL_MOVED, ge.getSource(), ge.getWhen(), modifiers, ge.getAllPointerTypes(), ge.getAllPointerIDs(), ge.getAllX(), ge.getAllY(), ge.getAllPressures(), ge.getMaxPressure(), ge.getButton(), ge.getClickCount(), rotationXYZ, scrollSlop); } return null; } public final float[] getScrollDistanceXY() { return scrollDistance; } @Override public boolean process(final InputEvent in) { if( null != hitGestureEvent || !(in instanceof MouseEvent) ) { return true; } final MouseEvent pe = (MouseEvent)in; if( pe.getPointerType(0).getPointerClass() != MouseEvent.PointerClass.Onscreen ) { return false; } pointerDownCount = pe.getPointerCount(); final int eventType = pe.getEventType(); final int x0 = pe.getX(0); final int y0 = pe.getY(0); switch ( eventType ) { case MouseEvent.EVENT_MOUSE_PRESSED: { int gPtr = 0; if( ST_NONE == gestureState && 1 == pointerDownCount ) { pIds[0] = pe.getPointerId(0); pIds[1] = -1; gestureState = ST_1PRESS; } else if( ST_NONE < gestureState && 2 == pointerDownCount && 1 == gesturePointers(pe, 0) /* w/o pressed pointer */ ) { final int x1 = pe.getX(1); final int y1 = pe.getY(1); final int xm = (x0+x1)/2; final int ym = (y0+y1)/2; if( ST_1PRESS == gestureState ) { final int sqDist = getSquareDistance(x0, y0, x1, y1); final boolean isDistWithinDoubleTapSlop = sqDist < doubleTapSlopSquare; if( isDistWithinDoubleTapSlop ) { // very first 2-finger touch-down gPtr = 2; pIds[0] = pe.getPointerId(0); pIds[1] = pe.getPointerId(1); lastX = xm; lastY = ym; sqStartDist = sqDist; gestureState = ST_2PRESS_T; } if(DEBUG) { final int dist = (int)Math.round(Math.sqrt(sqDist)); System.err.println(this+".pressed.1: dist "+dist+", gPtr "+gPtr+", distWithin2DTSlop "+isDistWithinDoubleTapSlop+", last "+lastX+"/"+lastY+", "+pe); } } else if( ST_2PRESS_C == gestureState ) { // pick up gesture after temp loosing one pointer gPtr = gesturePointers(pe, -1); if( 2 == gPtr ) { // same pointers re-touch-down lastX = xm; lastY = ym; } else { // other 2 pointers .. should rarely happen! clear(true); } } } if(DEBUG) { System.err.println(this+".pressed: gPtr "+gPtr+", this "+lastX+"/"+lastY+", "+pe); } } break; case MouseEvent.EVENT_MOUSE_RELEASED: { pointerDownCount--; // lifted final int gPtr = gesturePointers(pe, 0); // w/o lifted pointer if ( 1 == gPtr ) { // tolerate lifting 1 of 2 gesture pointers temporary gestureState = ST_2PRESS_C; } else if( 0 == gPtr ) { // all lifted clear(true); } if(DEBUG) { System.err.println(this+".released: gPtr "+gPtr+", "+pe); } } break; case MouseEvent.EVENT_MOUSE_DRAGGED: { if( 2 == pointerDownCount && ST_1PRESS < gestureState ) { final int gPtr = gesturePointers(pe, -1); if( 2 == gPtr ) { // same pointers final int x1 = pe.getX(1); final int y1 = pe.getY(1); final int xm = (x0+x1)/2; final int ym = (y0+y1)/2; final int sqDist = getSquareDistance(x0, y0, x1, y1); final boolean isDistDiffWithinDoubleTapSlop = Math.abs(sqDist - sqStartDist) <= doubleTapSlopSquare; if( isDistDiffWithinDoubleTapSlop ) { switch( gestureState ) { case ST_2PRESS_T: { final int sqScrollLen = getSquareDistance(lastX, lastY, xm, ym); if( sqScrollLen > scrollSlopSquare ) { // min. scrolling threshold reached gestureState = ST_SCROLL; } } break; case ST_2PRESS_C: gestureState = ST_SCROLL; break; case ST_SCROLL: scrollDistance[0] = lastX - xm; scrollDistance[1] = lastY - ym; hitGestureEvent = pe; break; } if(DEBUG) { final boolean isDistWithinDoubleTapSlop = sqDist < doubleTapSlopSquare; final int dist = (int)Math.round(Math.sqrt(sqDist)); final int sqScrollLen = getSquareDistance(lastX, lastY, xm, ym); final int scrollLen = (int)Math.round(Math.sqrt(sqScrollLen)); System.err.println(this+".dragged.1: pDist "+dist+", scrollLen "+scrollLen+", gPtr "+gPtr+" ["+pIds[0]+", "+pIds[1]+"]"+ ", diffDistWithinTapSlop "+isDistDiffWithinDoubleTapSlop+ ", distWithin2DTSlop "+isDistWithinDoubleTapSlop+ ", this "+xm+"/"+ym+", last "+lastX+"/"+lastY+", d "+scrollDistance[0]+"/"+scrollDistance[1]); } } else { // distance too big .. if(DEBUG) { final boolean isDistWithinDoubleTapSlop = sqDist < doubleTapSlopSquare; final int dist = (int)Math.round(Math.sqrt(sqDist)); final int startDist = (int)Math.round(Math.sqrt(sqStartDist)); System.err.println(this+".dragged.X1: pDist "+dist+", distStart "+startDist+", gPtr "+gPtr+" ["+pIds[0]+", "+pIds[1]+"]"+ ", diffDistWithinTapSlop "+isDistDiffWithinDoubleTapSlop+ ", distWithin2DTSlop "+isDistWithinDoubleTapSlop+ ", this "+xm+"/"+ym+", last "+lastX+"/"+lastY+", d "+scrollDistance[0]+"/"+scrollDistance[1]); } clear(true); } if( ST_2PRESS_T < gestureState ) { // state ST_2PRESS_T waits for min scroll threshold ! lastX = xm; lastY = ym; } } else { // other 2 pointers .. should rarely happen! if(DEBUG) { System.err.println(this+".dragged.X2: gPtr "+gPtr+" ["+pIds[0]+", "+pIds[1]+"]"+ ", last "+lastX+"/"+lastY+", d "+scrollDistance[0]+"/"+scrollDistance[1]); } clear(true); } } } break; default: } return null != hitGestureEvent; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy