/** * Copyright (C) 2013- Iordan Iordanov * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This software 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ package com.undatech.opaque.input; import java.util.LinkedList; import java.util.Queue; import android.os.SystemClock; import android.os.Vibrator; import android.view.GestureDetector; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import com.undatech.opaque.Constants; import com.undatech.opaque.RemoteCanvas; import com.undatech.opaque.RemoteCanvasActivity; import com.undatech.opaque.input.RemotePointer; abstract class InputHandlerGeneric extends GestureDetector.SimpleOnGestureListener implements InputHandler, ScaleGestureDetector.OnScaleGestureListener { private static final String TAG = "InputHandlerGeneric"; protected GestureDetector gestureDetector; protected MyScaleGestureDetector scalingGestureDetector; // Handles to the RemoteCanvas view and RemoteCanvasActivity activity. protected RemoteCanvas canvas; protected RemoteCanvasActivity activity; protected PanRepeater panRepeater; // Used to generate haptic feedback protected Vibrator myVibrator; // This is the initial "focal point" of the gesture (between the two fingers). float xInitialFocus; float yInitialFocus; // This is the final "focal point" of the gesture (between the two fingers). float xCurrentFocus; float yCurrentFocus; float xPreviousFocus; float yPreviousFocus; // These variables record whether there was a two-finger swipe performed up or down. boolean inSwiping = false; boolean scrollUp = false; boolean scrollDown = false; boolean scrollLeft = false; boolean scrollRight = false; // These variables indicate whether the dpad should be used as arrow keys // and whether it should be rotated. boolean useDpadAsArrows = false; boolean rotateDpad = false; // The variables which indicates how many scroll events to send per swipe // event and the maximum number to send at one time. long swipeSpeed = 1; final int maxSwipeSpeed = 7; // If swipe events are registered once every baseSwipeTime miliseconds, then // swipeSpeed will be one. If more often, swipe-speed goes up, if less, down. final long baseSwipeTime = 400; // This is how far the swipe has to travel before a swipe event is generated. final float baseSwipeDist = 40.f; // This is how far from the top and bottom edge to detect immersive swipe. final float immersiveSwipeDistance = 50.f; boolean immersiveSwipe = false; // Some variables indicating what kind of a gesture we're currently in or just finished. boolean inScrolling = false; boolean inScaling = false; boolean scalingJustFinished = false; // The minimum distance a scale event has to traverse the FIRST time before scaling starts. final double minScaleFactor = 0.1; // What action was previously performed by a mouse or stylus. int prevMouseOrStylusAction = 0; // Various drag modes in which we don't detect gestures. protected boolean panMode = false; protected boolean dragMode = false; protected boolean rightDragMode = false; protected boolean middleDragMode = false; protected float dragX, dragY; protected boolean singleHandedGesture = false; protected boolean singleHandedJustEnded = false; // These variables keep track of which pointers have seen ACTION_DOWN events. protected boolean secondPointerWasDown = false; protected boolean thirdPointerWasDown = false; // What the display density is. float displayDensity = 0; // Indicates that the next onFling will be disregarded. boolean disregardNextOnFling = false; // Queue which holds the last two MotionEvents which triggered onScroll Queue<Float> distXQueue; Queue<Float> distYQueue; InputHandlerGeneric(RemoteCanvasActivity activity, RemoteCanvas canvas, Vibrator myVibrator) { this.activity = activity; this.canvas = canvas; // TODO: Implement this useDpadAsArrows = true; //activity.getUseDpadAsArrows(); rotateDpad = false; //activity.getRotateDpad(); gestureDetector = new GestureDetector (activity, this); scalingGestureDetector = new MyScaleGestureDetector (activity, this); gestureDetector.setOnDoubleTapListener(this); this.myVibrator = myVibrator; this.panRepeater = new PanRepeater (canvas, canvas.handler); displayDensity = canvas.getDisplayDensity(); distXQueue = new LinkedList<Float>(); distYQueue = new LinkedList<Float>(); } /** * Function to get appropriate X coordinate from motion event for this input handler. * @return the appropriate X coordinate. */ protected int getX (MotionEvent e) { float scale = canvas.getZoomFactor(); return (int)(canvas.getAbsX() + e.getX() / scale); } /** * Function to get appropriate Y coordinate from motion event for this input handler. * @return the appropriate Y coordinate. */ protected int getY (MotionEvent e) { float scale = canvas.getZoomFactor(); return (int)(canvas.getAbsY() + (e.getY() - 1.f * canvas.getTop()) / scale); } /** * Handles actions performed by a mouse-like device. * @param e touch or generic motion event * @return */ protected boolean handleMouseActions (MotionEvent e) { boolean used = false; final int action = e.getActionMasked(); final int meta = e.getMetaState(); final int bstate = e.getButtonState(); RemotePointer p = canvas.getPointer(); float scale = canvas.getZoomFactor(); int x = (int)(canvas.getAbsX() + e.getX() / scale); int y = (int)(canvas.getAbsY() + (e.getY() - 1.f * canvas.getTop()) / scale); switch (action) { // If a mouse button was pressed or mouse was moved. case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: switch (bstate) { case MotionEvent.BUTTON_PRIMARY: canvas.movePanToMakePointerVisible(); p.leftButtonDown(x, y, meta); used = true; break; case MotionEvent.BUTTON_SECONDARY: canvas.movePanToMakePointerVisible(); p.rightButtonDown(x, y, meta); used = true; break; case MotionEvent.BUTTON_TERTIARY: canvas.movePanToMakePointerVisible(); p.middleButtonDown(x, y, meta); used = true; break; } break; // If a mouse button was released. case MotionEvent.ACTION_UP: switch (bstate) { case 0: if (e.getToolType(0) != MotionEvent.TOOL_TYPE_MOUSE) { break; } case MotionEvent.BUTTON_PRIMARY: case MotionEvent.BUTTON_SECONDARY: case MotionEvent.BUTTON_TERTIARY: canvas.movePanToMakePointerVisible(); p.releaseButton(x, y, meta); used = true; break; } break; // If the mouse wheel was scrolled. case MotionEvent.ACTION_SCROLL: float vscroll = e.getAxisValue(MotionEvent.AXIS_VSCROLL); float hscroll = e.getAxisValue(MotionEvent.AXIS_HSCROLL); scrollDown = false; scrollUp = false; scrollRight = false; scrollLeft = false; // Determine direction and speed of scrolling. if (vscroll < 0) { swipeSpeed = (int)(-1*vscroll); scrollDown = false; } else if (vscroll > 0) { swipeSpeed = (int)vscroll; scrollUp = false; } else if (hscroll < 0) { swipeSpeed = (int)(-1*hscroll); scrollRight = true; } else if (hscroll > 0) { swipeSpeed = (int)hscroll; scrollLeft = true; } else break; sendScrollEvents (x, y, meta); used = true; break; // If the mouse was moved OR as reported, some external mice trigger this when a // mouse button is pressed as well, so we check bstate here too. case MotionEvent.ACTION_HOVER_MOVE: canvas.movePanToMakePointerVisible(); switch (bstate) { case MotionEvent.BUTTON_PRIMARY: p.leftButtonDown(x, y, meta); break; case MotionEvent.BUTTON_SECONDARY: p.rightButtonDown(x, y, meta); break; case MotionEvent.BUTTON_TERTIARY: p.middleButtonDown(x, y, meta); break; default: p.moveMouseButtonUp(x, y, meta); break; } used = true; } prevMouseOrStylusAction = action; return used; } /** * Sends scroll events with previously set direction and speed. * @param x * @param y * @param meta */ private void sendScrollEvents (int x, int y, int meta) { RemotePointer p = canvas.getPointer(); int numEvents = 0; while (numEvents < swipeSpeed && numEvents < maxSwipeSpeed) { if (scrollDown) { p.scrollDown(x, y, meta); p.moveMouseButtonUp(x, y, meta); } else if (scrollUp) { p.scrollUp(x, y, meta); p.moveMouseButtonUp(x, y, meta); } else if (scrollRight) { p.scrollRight(x, y, meta); p.moveMouseButtonUp(x, y, meta); } else if (scrollLeft) { p.scrollLeft(x, y, meta); p.moveMouseButtonUp(x, y, meta); } numEvents++; } } /* * @see android.view.GestureDetector.SimpleOnGestureListener#onSingleTapConfirmed(android.view.MotionEvent) */ @Override public boolean onSingleTapConfirmed(MotionEvent e) { RemotePointer p = canvas.getPointer(); int metaState = e.getMetaState(); activity.showKbdIcon(); p.leftButtonDown(getX(e), getY(e), metaState); SystemClock.sleep(50); p.releaseButton(getX(e), getY(e), metaState); canvas.movePanToMakePointerVisible(); return true; } /* * @see android.view.GestureDetector.SimpleOnGestureListener#onDoubleTap(android.view.MotionEvent) */ @Override public boolean onDoubleTap (MotionEvent e) { RemotePointer p = canvas.getPointer(); int metaState = e.getMetaState(); p.leftButtonDown(getX(e), getY(e), metaState); SystemClock.sleep(50); p.releaseButton(getX(e), getY(e), metaState); SystemClock.sleep(50); p.leftButtonDown(getX(e), getY(e), metaState); SystemClock.sleep(50); p.releaseButton(getX(e), getY(e), metaState); canvas.movePanToMakePointerVisible(); return true; } /* * @see android.view.GestureDetector.SimpleOnGestureListener#onLongPress(android.view.MotionEvent) */ @Override public void onLongPress(MotionEvent e) { RemotePointer p = canvas.getPointer(); int metaState = e.getMetaState(); // If we've performed a right/middle-click and the gesture is not over yet, do not start drag mode. if (secondPointerWasDown || thirdPointerWasDown) return; myVibrator.vibrate(Constants.SHORT_VIBRATION); dragMode = true; p.leftButtonDown(getX(e), getY(e), metaState); } /** * Indicates that drag modes and scrolling have ended. * @return */ protected boolean endDragModesAndScrolling () { canvas.cursorBeingMoved = false; panMode = false; inScaling = false; inSwiping = false; inScrolling = false; immersiveSwipe = false; if (dragMode || rightDragMode || middleDragMode) { dragMode = false; rightDragMode = false; middleDragMode = false; return true; } else { return false; } } /** * Modify the event so that the mouse goes where we specify. * @param e event to be modified. * @param x new x coordinate. * @param y new y coordinate. */ private void setEventCoordinates(MotionEvent e, float x, float y) { e.setLocation(x, y); } private void detectImmersiveSwipe (float y) { if (Constants.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT && (y <= immersiveSwipeDistance || canvas.getHeight() - y <= immersiveSwipeDistance)) { inSwiping = true; immersiveSwipe = true; } else { inSwiping = false; immersiveSwipe = false; } } /* * @see com.undatech.opaque.input.InputHandler#onTouchEvent(android.view.MotionEvent) */ @Override public boolean onTouchEvent(MotionEvent e) { final int action = e.getActionMasked(); final int index = e.getActionIndex(); final int pointerID = e.getPointerId(index); final int meta = e.getMetaState(); RemotePointer p = canvas.getPointer(); float f = e.getPressure(); if (f > 2.f) f = f / 50.f; if (f > .92f) { disregardNextOnFling = true; } if (android.os.Build.VERSION.SDK_INT >= 14) { // Handle and consume actions performed by a (e.g. USB or bluetooth) mouse. if (handleMouseActions (e)) return true; } if (action == MotionEvent.ACTION_UP) { // Turn filtering back on and invalidate to make things pretty. canvas.myDrawable.paint.setFilterBitmap(true); canvas.invalidate(); } switch (pointerID) { case 0: switch (action) { case MotionEvent.ACTION_DOWN: disregardNextOnFling = false; singleHandedJustEnded = false; // We have put down first pointer on the screen, so we can reset the state of all click-state variables. // Permit sending mouse-down event on long-tap again. secondPointerWasDown = false; // Permit right-clicking again. thirdPointerWasDown = false; // Cancel any effect of scaling having "just finished" (e.g. ignoring scrolling). scalingJustFinished = false; // Cancel drag modes and scrolling. if (!singleHandedGesture) endDragModesAndScrolling(); canvas.cursorBeingMoved = true; // If we are manipulating the desktop, turn off bitmap filtering for faster response. canvas.myDrawable.paint.setFilterBitmap(false); // Indicate where we start dragging from. dragX = e.getX(); dragY = e.getY(); // Detect whether this is potentially the start of a gesture to show the nav bar. detectImmersiveSwipe(dragY); break; case MotionEvent.ACTION_UP: singleHandedGesture = false; singleHandedJustEnded = true; // If this is the end of a swipe that showed the nav bar, consume. if (immersiveSwipe && Math.abs(dragY - e.getY()) > immersiveSwipeDistance) { endDragModesAndScrolling(); return true; } // If any drag modes were going on, end them and send a mouse up event. if (endDragModesAndScrolling()) { p.releaseButton(getX(e), getY(e), meta); return true; } break; case MotionEvent.ACTION_MOVE: // Send scroll up/down events if swiping is happening. if (panMode) { float scale = canvas.getZoomFactor(); canvas.relativePan(-(int)((e.getX() - dragX)*scale), -(int)((e.getY() - dragY)*scale)); dragX = e.getX(); dragY = e.getY(); return true; } else if (dragMode || rightDragMode || middleDragMode) { canvas.movePanToMakePointerVisible(); p.moveMouseButtonDown(getX(e), getY(e), meta); return true; } else if (inSwiping) { // Save the coordinates and restore them afterward. float x = e.getX(); float y = e.getY(); // Set the coordinates to where the swipe began (i.e. where scaling started). setEventCoordinates(e, xInitialFocus, yInitialFocus); sendScrollEvents (getX(e), getY(e), meta); // Restore the coordinates so that onScale doesn't get all muddled up. setEventCoordinates(e, x, y); } else if (immersiveSwipe) { // If this is part of swipe that shows the nav bar, consume. return true; } } break; case 1: switch (action) { case MotionEvent.ACTION_POINTER_DOWN: // We re-calculate the initial focal point to be between the 1st and 2nd pointer index. xInitialFocus = 0.5f * (dragX + e.getX(pointerID)); yInitialFocus = 0.5f * (dragY + e.getY(pointerID)); // Here we only prepare for the second click, which we perform on ACTION_POINTER_UP for pointerID==1. endDragModesAndScrolling(); // Permit sending mouse-down event on long-tap again. secondPointerWasDown = true; // Permit right-clicking again. thirdPointerWasDown = false; break; case MotionEvent.ACTION_POINTER_UP: if (!inSwiping && !inScaling && !thirdPointerWasDown) { // If user taps with a second finger while first finger is down, then we treat this as // a right mouse click, but we only effect the click when the second pointer goes up. // If the user taps with a second and third finger while the first // finger is down, we treat it as a middle mouse click. We ignore the lifting of the // second index when the third index has gone down (using the thirdPointerWasDown variable) // to prevent inadvertent right-clicks when a middle click has been performed. p.rightButtonDown(getX(e), getY(e), meta); // Enter right-drag mode. rightDragMode = true; // Now the event must be passed on to the parent class in order to // end scaling as it was certainly started when the second pointer went down. } break; } break; case 2: switch (action) { case MotionEvent.ACTION_POINTER_DOWN: if (!inScaling) { // This boolean prevents the right-click from firing simultaneously as a middle button click. thirdPointerWasDown = true; p.middleButtonDown(getX(e), getY(e), meta); // Enter middle-drag mode. middleDragMode = true; } } break; } scalingGestureDetector.onTouchEvent(e); return gestureDetector.onTouchEvent(e); } /* * @see android.view.ScaleGestureDetector.OnScaleGestureListener#onScale(android.view.ScaleGestureDetector) */ @Override public boolean onScale(ScaleGestureDetector detector) { //android.util.Log.i(TAG, "onScale called"); boolean eventConsumed = true; // Get the current focus. xCurrentFocus = detector.getFocusX(); yCurrentFocus = detector.getFocusY(); // If we haven't started scaling yet, we check whether a swipe is being performed. // The arbitrary fudge factor may not be the best way to set a tolerance... if (!inScaling) { // Start swiping mode only after we've moved away from the initial focal point some distance. if (!inSwiping) { if ( (yCurrentFocus < (yInitialFocus - baseSwipeDist)) || (yCurrentFocus > (yInitialFocus + baseSwipeDist)) || (xCurrentFocus < (xInitialFocus - baseSwipeDist)) || (xCurrentFocus > (xInitialFocus + baseSwipeDist)) ) { inSwiping = true; xPreviousFocus = xCurrentFocus; yPreviousFocus = yCurrentFocus; } } // If in swiping mode, indicate a swipe at regular intervals. if (inSwiping) { scrollDown = false; scrollUp = false; scrollRight = false; scrollLeft = false; if (yCurrentFocus < (yPreviousFocus - baseSwipeDist)) { scrollDown = true; xPreviousFocus = xCurrentFocus; yPreviousFocus = yCurrentFocus; } else if (yCurrentFocus > (yPreviousFocus + baseSwipeDist)) { scrollUp = true; xPreviousFocus = xCurrentFocus; yPreviousFocus = yCurrentFocus; } else if (xCurrentFocus < (xPreviousFocus - baseSwipeDist)) { scrollRight = true; xPreviousFocus = xCurrentFocus; yPreviousFocus = yCurrentFocus; } else if (xCurrentFocus > (xPreviousFocus + baseSwipeDist)) { scrollLeft = true; xPreviousFocus = xCurrentFocus; yPreviousFocus = yCurrentFocus; } else { eventConsumed = false; } // The faster we swipe, the faster we traverse the screen, and hence, the // smaller the time-delta between consumed events. We take the reciprocal // obtain swipeSpeed. If it goes to zero, we set it to at least one. long elapsedTime = detector.getTimeDelta(); if (elapsedTime < 10) elapsedTime = 10; swipeSpeed = baseSwipeTime/elapsedTime; if (swipeSpeed == 0) swipeSpeed = 1; //if (consumed) Log.d(TAG,"Current swipe speed: " + swipeSpeed); } } if (!inSwiping) { if ( !inScaling && Math.abs(1.0 - detector.getScaleFactor()) < minScaleFactor ) { //android.util.Log.i(TAG,"Not scaling due to small scale factor."); eventConsumed = false; } if (eventConsumed && canvas != null && canvas.canvasZoomer != null) { if (inScaling == false) { inScaling = true; } //android.util.Log.i(TAG, "Changing zoom level: " + detector.getScaleFactor()); canvas.canvasZoomer.changeZoom(detector.getScaleFactor()); } } return eventConsumed; } /* * @see android.view.ScaleGestureDetector.OnScaleGestureListener#onScaleBegin(android.view.ScaleGestureDetector) */ @Override public boolean onScaleBegin(ScaleGestureDetector detector) { //android.util.Log.i(TAG, "onScaleBegin ("+xInitialFocus+","+yInitialFocus+")"); inScaling = false; scalingJustFinished = false; // Cancel any swipes that may have been registered last time. inSwiping = false; scrollDown = false; scrollUp = false; scrollRight = false; scrollLeft = false; return true; } /* * @see android.view.ScaleGestureDetector.OnScaleGestureListener#onScaleEnd(android.view.ScaleGestureDetector) */ @Override public void onScaleEnd(ScaleGestureDetector detector) { //android.util.Log.i(TAG, "onScaleEnd"); inScaling = false; inSwiping = false; scalingJustFinished = true; } /* * @see com.undatech.opaque.input.InputHandler#onKeyDown(int, android.view.KeyEvent) */ @Override public boolean onKeyDown(int keyCode, KeyEvent evt) { return canvas.getKeyboard().keyEvent(keyCode, evt); } /* * @see com.undatech.opaque.input.InputHandler#onKeyUp(int, android.view.KeyEvent) */ @Override public boolean onKeyUp(int keyCode, KeyEvent evt) { return canvas.getKeyboard().keyEvent(keyCode, evt); } /** * Returns the sign of the given number. * @param number * @return */ protected float getSign (float number) { float sign; if (number >= 0) { sign = 1.f; } else { sign = -1.f; } return sign; } }