@Override
  public boolean onTouchEvent(MotionEvent event) {
    // Avoid short-circuit logic evaluation - ensure all gesture detectors see all events so
    // that they generate correct notifications.
    boolean handled = mScroller.onTouchEvent(event);
    handled |= mZoomer.onTouchEvent(event);
    handled |= mTapDetector.onTouchEvent(event);
    mSwipePinchDetector.onTouchEvent(event);

    switch (event.getActionMasked()) {
      case MotionEvent.ACTION_DOWN:
        mViewer.setAnimationEnabled(false);
        mSuppressCursorMovement = false;
        mSuppressFling = false;
        mSwipeCompleted = false;
        break;

      case MotionEvent.ACTION_POINTER_DOWN:
        mTotalMotionY = 0;
        break;

      case MotionEvent.ACTION_UP:
        releaseAnyHeldButton();
        break;

      default:
        break;
    }
    return handled;
  }
  /** Processes a (multi-finger) swipe gesture. */
  private boolean onSwipe() {
    if (mTotalMotionY > mSwipeThreshold) {
      // Swipe down occurred.
      mViewer.showActionBar();
    } else if (mTotalMotionY < -mSwipeThreshold) {
      // Swipe up occurred.
      mViewer.showKeyboard();
    } else {
      return false;
    }

    mSuppressCursorMovement = true;
    mSuppressFling = true;
    mSwipeCompleted = true;
    return true;
  }
  /** Moves the mouse-cursor, injects a mouse-move event and repositions the image. */
  private void moveCursor(float newX, float newY) {
    synchronized (mRenderData) {
      // Constrain cursor to the image area.
      if (newX < 0) newX = 0;
      if (newY < 0) newY = 0;
      if (newX > mRenderData.imageWidth) newX = mRenderData.imageWidth;
      if (newY > mRenderData.imageHeight) newY = mRenderData.imageHeight;
      mCursorPosition.set(newX, newY);
      repositionImage();
    }

    mViewer.injectMouseEvent((int) newX, (int) newY, BUTTON_UNDEFINED, false);
  }
  /**
   * Repositions the image by translating it (without affecting the zoom level) to place the cursor
   * close to the center of the screen.
   */
  private void repositionImage() {
    synchronized (mRenderData) {
      // Get the current cursor position in screen coordinates.
      float[] cursorScreen = {mCursorPosition.x, mCursorPosition.y};
      mRenderData.transform.mapPoints(cursorScreen);

      // Translate so the cursor is displayed in the middle of the screen.
      mRenderData.transform.postTranslate(
          (float) mRenderData.screenWidth / 2 - cursorScreen[0],
          (float) mRenderData.screenHeight / 2 - cursorScreen[1]);

      // Now the cursor is displayed in the middle of the screen, see if the image can be
      // panned so that more of it is visible. The primary goal is to show as much of the
      // image as possible. The secondary goal is to keep the cursor in the middle.

      // Get the coordinates of the desktop rectangle (top-left/bottom-right corners) in
      // screen coordinates. Order is: left, top, right, bottom.
      float[] rectScreen = {0, 0, mRenderData.imageWidth, mRenderData.imageHeight};
      mRenderData.transform.mapPoints(rectScreen);

      float leftDelta = rectScreen[0];
      float rightDelta = rectScreen[2] - mRenderData.screenWidth;
      float topDelta = rectScreen[1];
      float bottomDelta = rectScreen[3] - mRenderData.screenHeight;
      float xAdjust = 0;
      float yAdjust = 0;

      if (rectScreen[2] - rectScreen[0] < mRenderData.screenWidth) {
        // Image is narrower than the screen, so center it.
        xAdjust = -(rightDelta + leftDelta) / 2;
      } else if (leftDelta > 0 && rightDelta > 0) {
        // Panning the image left will show more of it.
        xAdjust = -Math.min(leftDelta, rightDelta);
      } else if (leftDelta < 0 && rightDelta < 0) {
        // Pan the image right.
        xAdjust = Math.min(-leftDelta, -rightDelta);
      }

      // Apply similar logic for yAdjust.
      if (rectScreen[3] - rectScreen[1] < mRenderData.screenHeight) {
        yAdjust = -(bottomDelta + topDelta) / 2;
      } else if (topDelta > 0 && bottomDelta > 0) {
        yAdjust = -Math.min(topDelta, bottomDelta);
      } else if (topDelta < 0 && bottomDelta < 0) {
        yAdjust = Math.min(-topDelta, -bottomDelta);
      }

      mRenderData.transform.postTranslate(xAdjust, yAdjust);
    }
    mViewer.transformationChanged();
  }
  @Override
  public void processAnimation() {
    int previousX = mFlingScroller.getCurrX();
    int previousY = mFlingScroller.getCurrY();
    if (!mFlingScroller.computeScrollOffset()) {
      mViewer.setAnimationEnabled(false);
      return;
    }
    int deltaX = mFlingScroller.getCurrX() - previousX;
    int deltaY = mFlingScroller.getCurrY() - previousY;
    float[] delta = {deltaX, deltaY};
    synchronized (mRenderData) {
      Matrix canvasToImage = new Matrix();
      mRenderData.transform.invert(canvasToImage);
      canvasToImage.mapVectors(delta);
    }

    moveCursor(mCursorPosition.x + delta[0], mCursorPosition.y + delta[1]);
  }
 /** Injects a button event using the current cursor location. */
 private void injectButtonEvent(int button, boolean pressed) {
   mViewer.injectMouseEvent((int) mCursorPosition.x, (int) mCursorPosition.y, button, pressed);
 }