/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * 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. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "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 THE COPYRIGHT OWNER 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. */ package com.jme3.input.android; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import com.jme3.input.event.InputEvent; import com.jme3.input.event.MouseMotionEvent; import com.jme3.input.event.TouchEvent; import java.util.logging.Level; import java.util.logging.Logger; /** * AndroidGestureHandler uses Gesture type listeners to create jME TouchEvents * for gestures. This class is designed to handle the gestures supported * on Android rev 9 (Android 2.3). Extend this class to add functionality * added by Android after rev 9. * * @author iwgeric */ public class AndroidGestureHandler implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, ScaleGestureDetector.OnScaleGestureListener { private static final Logger logger = Logger.getLogger(AndroidGestureHandler.class.getName()); private AndroidInputHandler androidInput; private GestureDetector gestureDetector; private ScaleGestureDetector scaleDetector; float gestureDownX = -1f; float gestureDownY = -1f; float scaleStartX = -1f; float scaleStartY = -1f; public AndroidGestureHandler(AndroidInputHandler androidInput) { this.androidInput = androidInput; } public void initialize() { } public void destroy() { setView(null); } public void setView(View view) { if (view != null) { gestureDetector = new GestureDetector(view.getContext(), this); scaleDetector = new ScaleGestureDetector(view.getContext(), this); } else { gestureDetector = null; scaleDetector = null; } } public void detectGesture(MotionEvent event) { if (gestureDetector != null && scaleDetector != null) { gestureDetector.onTouchEvent(event); scaleDetector.onTouchEvent(event); } } private int getPointerIndex(MotionEvent event) { return (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; } private int getPointerId(MotionEvent event) { return event.getPointerId(getPointerIndex(event)); } private void processEvent(TouchEvent event) { // Add the touch event androidInput.addEvent(event); if (androidInput.isSimulateMouse()) { InputEvent mouseEvent = generateMouseEvent(event); if (mouseEvent != null) { // Add the mouse event androidInput.addEvent(mouseEvent); } } } // TODO: Ring Buffer for mouse events? private InputEvent generateMouseEvent(TouchEvent event) { InputEvent inputEvent = null; int newX; int newY; int newDX; int newDY; if (androidInput.isMouseEventsInvertX()) { newX = (int) (androidInput.invertX(event.getX())); newDX = (int)event.getDeltaX() * -1; } else { newX = (int) event.getX(); newDX = (int)event.getDeltaX(); } int wheel = (int) (event.getScaleSpan()); // might need to scale to match mouse wheel int dWheel = (int) (event.getDeltaScaleSpan()); // might need to scale to match mouse wheel if (androidInput.isMouseEventsInvertY()) { newY = (int) (androidInput.invertY(event.getY())); newDY = (int)event.getDeltaY() * -1; } else { newY = (int) event.getY(); newDY = (int)event.getDeltaY(); } switch (event.getType()) { case SCALE_MOVE: inputEvent = new MouseMotionEvent(newX, newY, newDX, newDY, wheel, dWheel); inputEvent.setTime(event.getTime()); break; } return inputEvent; } /* Events from onGestureListener */ public boolean onDown(MotionEvent event) { // start of all GestureListeners. Not really a gesture by itself // so we don't create an event. // However, reset the scaleInProgress here since this is the beginning // of a series of gesture events. // logger.log(Level.INFO, "onDown pointerId: {0}, action: {1}, x: {2}, y: {3}", // new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); gestureDownX = androidInput.getJmeX(event.getX()); gestureDownY = androidInput.invertY(androidInput.getJmeY(event.getY())); return true; } public boolean onSingleTapUp(MotionEvent event) { // Up of single tap. May be followed by a double tap later. // use onSingleTapConfirmed instead. // logger.log(Level.INFO, "onSingleTapUp pointerId: {0}, action: {1}, x: {2}, y: {3}", // new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); return true; } public void onShowPress(MotionEvent event) { // logger.log(Level.INFO, "onShowPress pointerId: {0}, action: {1}, x: {2}, y: {3}", // new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); float jmeX = androidInput.getJmeX(event.getX()); float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.SHOWPRESS, jmeX, jmeY, 0, 0); touchEvent.setPointerId(getPointerId(event)); touchEvent.setTime(event.getEventTime()); touchEvent.setPressure(event.getPressure()); processEvent(touchEvent); } public void onLongPress(MotionEvent event) { // logger.log(Level.INFO, "onLongPress pointerId: {0}, action: {1}, x: {2}, y: {3}", // new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); float jmeX = androidInput.getJmeX(event.getX()); float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.LONGPRESSED, jmeX, jmeY, 0, 0); touchEvent.setPointerId(getPointerId(event)); touchEvent.setTime(event.getEventTime()); touchEvent.setPressure(event.getPressure()); processEvent(touchEvent); } public boolean onScroll(MotionEvent startEvent, MotionEvent endEvent, float distX, float distY) { // if not scaleInProgess, send scroll events. This is to avoid sending // scroll events when one of the fingers is lifted just before the other one. // Avoids sending the scroll for that brief period of time. // Return true so that the next event doesn't accumulate the distX and distY values. // Apparantly, both distX and distY are negative. // Negate distX to get the real value, but leave distY negative to compensate // for the fact that jME has y=0 at bottom where Android has y=0 at top. // if (!scaleInProgress) { if (!scaleDetector.isInProgress()) { // logger.log(Level.INFO, "onScroll pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, dx: {7}, dy: {8}", // new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), distX, distY}); float jmeX = androidInput.getJmeX(endEvent.getX()); float jmeY = androidInput.invertY(androidInput.getJmeY(endEvent.getY())); TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.SCROLL, jmeX, jmeY, androidInput.getJmeX(-distX), androidInput.getJmeY(distY)); touchEvent.setPointerId(getPointerId(endEvent)); touchEvent.setTime(endEvent.getEventTime()); touchEvent.setPressure(endEvent.getPressure()); processEvent(touchEvent); } return true; } public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float velocityX, float velocityY) { // Fling happens only once at the end of the gesture (all fingers up). // Fling returns the velocity of the finger movement in pixels/sec. // Therefore, the dX and dY values are actually velocity instead of distance values // Since this does not track the movement, use the start position and velocity values. // logger.log(Level.INFO, "onFling pointerId: {0}, startAction: {1}, startX: {2}, startY: {3}, endAction: {4}, endX: {5}, endY: {6}, velocityX: {7}, velocityY: {8}", // new Object[]{getPointerId(startEvent), getAction(startEvent), startEvent.getX(), startEvent.getY(), getAction(endEvent), endEvent.getX(), endEvent.getY(), velocityX, velocityY}); float jmeX = androidInput.getJmeX(startEvent.getX()); float jmeY = androidInput.invertY(androidInput.getJmeY(startEvent.getY())); TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.FLING, jmeX, jmeY, velocityX, velocityY); touchEvent.setPointerId(getPointerId(endEvent)); touchEvent.setTime(endEvent.getEventTime()); touchEvent.setPressure(endEvent.getPressure()); processEvent(touchEvent); return true; } /* Events from onDoubleTapListener */ public boolean onSingleTapConfirmed(MotionEvent event) { // Up of single tap when no double tap followed. // logger.log(Level.INFO, "onSingleTapConfirmed pointerId: {0}, action: {1}, x: {2}, y: {3}", // new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); float jmeX = androidInput.getJmeX(event.getX()); float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.TAP, jmeX, jmeY, 0, 0); touchEvent.setPointerId(getPointerId(event)); touchEvent.setTime(event.getEventTime()); touchEvent.setPressure(event.getPressure()); processEvent(touchEvent); return true; } public boolean onDoubleTap(MotionEvent event) { //The down motion event of the first tap of the double-tap // We could use this event to fire off a double tap event, or use // DoubleTapEvent with a check for the UP action // logger.log(Level.INFO, "onDoubleTap pointerId: {0}, action: {1}, x: {2}, y: {3}", // new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); float jmeX = androidInput.getJmeX(event.getX()); float jmeY = androidInput.invertY(androidInput.getJmeY(event.getY())); TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.DOUBLETAP, jmeX, jmeY, 0, 0); touchEvent.setPointerId(getPointerId(event)); touchEvent.setTime(event.getEventTime()); touchEvent.setPressure(event.getPressure()); processEvent(touchEvent); return true; } public boolean onDoubleTapEvent(MotionEvent event) { //Notified when an event within a double-tap gesture occurs, including the down, move(s), and up events. // this means it will get called multiple times for a single double tap // logger.log(Level.INFO, "onDoubleTapEvent pointerId: {0}, action: {1}, x: {2}, y: {3}", // new Object[]{getPointerId(event), getAction(event), event.getX(), event.getY()}); // if (getAction(event) == MotionEvent.ACTION_UP) { // TouchEvent touchEvent = touchEventPool.getNextFreeEvent(); // touchEvent.set(TouchEvent.Type.DOUBLETAP, event.getX(), androidInput.invertY(event.getY()), 0, 0); // touchEvent.setPointerId(getPointerId(event)); // touchEvent.setTime(event.getEventTime()); // touchEvent.setPressure(event.getPressure()); // processEvent(touchEvent); // } return true; } /* Events from ScaleGestureDetector */ public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { // Scale uses a focusX and focusY instead of x and y. Focus is the middle // of the fingers. Therefore, use the x and y values from the Down event // so that the x and y values don't jump to the middle position. // return true or all gestures for this beginning event will be discarded logger.log(Level.INFO, "onScaleBegin"); scaleStartX = gestureDownX; scaleStartY = gestureDownY; TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.SCALE_START, scaleStartX, scaleStartY, 0f, 0f); touchEvent.setPointerId(0); touchEvent.setTime(scaleGestureDetector.getEventTime()); touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan()); touchEvent.setDeltaScaleSpan(0f); touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor()); touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress()); processEvent(touchEvent); return true; } public boolean onScale(ScaleGestureDetector scaleGestureDetector) { // return true or all gestures for this event will be accumulated logger.log(Level.INFO, "onScale"); scaleStartX = gestureDownX; scaleStartY = gestureDownY; TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.SCALE_MOVE, scaleStartX, scaleStartY, 0f, 0f); touchEvent.setPointerId(0); touchEvent.setTime(scaleGestureDetector.getEventTime()); touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan()); touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan()); touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor()); touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress()); processEvent(touchEvent); return true; } public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) { logger.log(Level.INFO, "onScaleEnd"); scaleStartX = gestureDownX; scaleStartY = gestureDownY; TouchEvent touchEvent = androidInput.getFreeTouchEvent(); touchEvent.set(TouchEvent.Type.SCALE_END, scaleStartX, scaleStartY, 0f, 0f); touchEvent.setPointerId(0); touchEvent.setTime(scaleGestureDetector.getEventTime()); touchEvent.setScaleSpan(scaleGestureDetector.getCurrentSpan()); touchEvent.setDeltaScaleSpan(scaleGestureDetector.getCurrentSpan() - scaleGestureDetector.getPreviousSpan()); touchEvent.setScaleFactor(scaleGestureDetector.getScaleFactor()); touchEvent.setScaleSpanInProgress(scaleDetector.isInProgress()); processEvent(touchEvent); } }