package com.bogdwellers.pinchtozoom; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Matrix; import android.graphics.PointF; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import com.bogdwellers.pinchtozoom.animation.FlingAnimatorHandler; import com.bogdwellers.pinchtozoom.animation.ScaleAnimatorHandler; /** * <p>The <code>ImageMatrixTouchHandler</code> enables pinch-zoom, pinch-rotate and dragging on an <code>ImageView</code>. * Registering an instance of this class to an <code>ImageView</code> is the only thing you need to do.</p> * * TODO Make event methods (for easy overriding) * * @author Martin * */ public class ImageMatrixTouchHandler extends MultiTouchListener { public static final int NONE = 0; public static final int DRAG = 1; public static final int PINCH = 2; public static final int MORPH = 3; // TODO For three or more touch points private static final float MIN_PINCH_DIST_PIXELS = 10f; public static final String TAG = ImageMatrixTouchHandler.class.getSimpleName(); /* * Attributes */ private ImageMatrixCorrector corrector; private Matrix savedMatrix; private int mode; private PointF startMid; private PointF mid; private float startSpacing; private float startAngle; private float pinchVelocity; private boolean rotateEnabled; private boolean scaleEnabled; private boolean translateEnabled; private boolean dragOnPinchEnabled; private long doubleTapZoomDuration; private long flingDuration; private long zoomReleaseDuration; private long pinchVelocityWindow; private float doubleTapZoomFactor; private float doubleTapZoomOutFactor; private float flingExaggeration; private float zoomReleaseExaggeration; private boolean updateTouchState; private GestureDetector gestureDetector; private ValueAnimator valueAnimator; /* * Constructor(s) */ public ImageMatrixTouchHandler(Context context) { this(context, new ImageViewerCorrector()); } public ImageMatrixTouchHandler(Context context, ImageMatrixCorrector corrector) { this.corrector = corrector; this.savedMatrix = new Matrix(); this.mode = NONE; this.startMid = new PointF(); this.mid = new PointF(); this.startSpacing = 1f; this.startAngle = 0f; this.rotateEnabled = false; this.scaleEnabled = true; this.translateEnabled = true; this.dragOnPinchEnabled = true; this.pinchVelocityWindow = 100; this.doubleTapZoomDuration = 200; this.flingDuration = 200; this.zoomReleaseDuration = 200; this.zoomReleaseExaggeration = 1.337f; this.flingExaggeration = 0.1337f; this.doubleTapZoomFactor = 2.5f; this.doubleTapZoomOutFactor = 1.4f; ImageGestureListener imageGestureListener = new ImageGestureListener(); this.gestureDetector = new GestureDetector(context, imageGestureListener); this.gestureDetector.setOnDoubleTapListener(imageGestureListener); } /* * Class methods */ /** * <p>Returns the mode the handler is currently in.</p> * @return */ public int getMode() { return mode; } /** * <p>Returns the <code>ImageMatrixCorrector</code> that corrects the image matrix when altered.</p> * @return */ public ImageMatrixCorrector getImageMatrixCorrector() { return corrector; } /** * <p>Updates the touch state during a touch event. That is, when touch mode is not {@link #NONE} .</p> * <p>Use this when the image in the <code>ImageView</code> or its matrix has been changed.</p> */ public void updateTouchState() { updateTouchState = true; } /** * <p>Indicates whether rotation is enabled.</p> * @return */ public boolean isRotateEnabled() { return rotateEnabled; } /** ** <p>Sets whether rotation is enabled.</p> * @param rotateEnabled */ public void setRotateEnabled(boolean rotateEnabled) { this.rotateEnabled = rotateEnabled; } /** * <p>Indicates whether scaling is enabled.</p> * @return */ public boolean isScaleEnabled() { return scaleEnabled; } /** * <p>Sets whether scaling is enabled.</p> * @param scaleEnabled */ public void setScaleEnabled(boolean scaleEnabled) { this.scaleEnabled = scaleEnabled; } /** * <p>Indicates whether translation is enabled.</p> * @return */ public boolean isTranslateEnabled() { return translateEnabled; } /** * <p>Sets whether translation is enabled.</p> * @param translateEnabled */ public void setTranslateEnabled(boolean translateEnabled) { this.translateEnabled = translateEnabled; } /** * <p>Indicates whether drag-on-pinch is enabled.</p> * @return */ public boolean isDragOnPinchEnabled() { return dragOnPinchEnabled; } /** * <p>Sets whether drag-on-pinch is enabled.</p> * @param dragOnPinchEnabled */ public void setDragOnPinchEnabled(boolean dragOnPinchEnabled) { this.dragOnPinchEnabled = dragOnPinchEnabled; } /** * <p>Sets the pinch velocity window in milliseconds for determining the pinch velocity.</p> * <p><b>Note:</b> Only touch events in this temporal window are used to calculate pinch velocity.</p> * @param pinchVelocityWindow */ public void setPinchVelocityWindow(long pinchVelocityWindow) { this.pinchVelocityWindow = pinchVelocityWindow; } /** * <p>Sets the double tap zoom animation duration. Setting the duration to <code>0</code> disables the animation altogether.</p> * @param doubleTapZoomDuration */ public void setDoubleTapZoomDuration(long doubleTapZoomDuration) { this.doubleTapZoomDuration = doubleTapZoomDuration; } /** * <p>Sets the fling animation duration. Setting the duration to <code>0</code> disables the animation altogether.</p> * @param flingDuration */ public void setFlingDuration(long flingDuration) { this.flingDuration = flingDuration; } /** * <p>Sets the zoom release animation duration. Setting the duration to <code>0</code> disables the animation altogether.</p> * @param zoomReleaseDuration */ public void setZoomReleaseDuration(long zoomReleaseDuration) { this.zoomReleaseDuration = zoomReleaseDuration; } /** * <p>Sets the double tap zoom factor.</p> * @param doubleTapZoomFactor */ public void setDoubleTapZoomFactor(float doubleTapZoomFactor) { this.doubleTapZoomFactor = doubleTapZoomFactor; } /** * <p>Sets the minimum scale factor when double tapping zooms back out instead of in.</p> * @param doubleTapZoomOutFactor */ public void setDoubleTapZoomOutFactor(float doubleTapZoomOutFactor) { this.doubleTapZoomOutFactor = doubleTapZoomOutFactor; } /** * <p>Sets the fling animation exaggeration factor.</p> * @param flingExaggeration */ public void setFlingExaggeration(float flingExaggeration) { this.flingExaggeration = flingExaggeration; } /** * <p>Sets the zoom release animation exaggeration factor.</p> * @param zoomReleaseExaggeration */ public void setZoomReleaseExaggeration(float zoomReleaseExaggeration) { this.zoomReleaseExaggeration = zoomReleaseExaggeration; } /** * <p>Indicates whether the image is being animated.</p> * @return */ public boolean isAnimating() { return valueAnimator != null && valueAnimator.isRunning(); } /** * <p>Cancels any running animations.</p> */ public void cancelAnimation() { if(isAnimating()) { valueAnimator.cancel(); } } /** * <p>Evaluates the touch state.</p> * @param event * @param matrix */ private void evaluateTouchState(MotionEvent event, Matrix matrix) { // Save the starting points updateStartPoints(event); savedMatrix.set(matrix); // Update the mode int touchCount = getTouchCount(); if(touchCount == 0) { mode = NONE; } else { if(isAnimating()) { valueAnimator.cancel(); } if(touchCount == 1) { if(mode == PINCH) { if(zoomReleaseDuration > 0 && !isAnimating()) { // Animate zoom release float scale = (float) Math.pow(Math.pow(Math.pow(pinchVelocity, 1d / 1000d), zoomReleaseDuration), zoomReleaseExaggeration); animateZoom(scale, zoomReleaseDuration, mid.x, mid.y, new DecelerateInterpolator()); } } mode = DRAG; } else if (touchCount > 1) { mode = PINCH; // Calculate the start distance startSpacing = spacing(event, getId(0), getId(1)); pinchVelocity = 0f; if(startSpacing > MIN_PINCH_DIST_PIXELS) { midPoint(startMid, event, getId(0), getId(1)); startAngle = angle(event, getId(0), getId(1), startedLower(getStartPoint(0), getStartPoint(1))); } } } } /* * Interface implementations */ @Override public boolean onTouch(View view, MotionEvent event) { super.onTouch(view, event); gestureDetector.onTouchEvent(event); ImageView imageView; try { imageView = (ImageView) view; } catch(ClassCastException e) { throw new IllegalStateException("View must be an instance of ImageView", e); } // Get the matrix Matrix matrix = imageView.getImageMatrix(); // Sets the image view if(corrector.getImageView() != imageView) { corrector.setImageView(imageView); } else if(imageView.getScaleType() != ScaleType.MATRIX) { imageView.setScaleType(ScaleType.MATRIX); corrector.setMatrix(matrix); } int actionMasked = event.getActionMasked(); switch (actionMasked) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: evaluateTouchState(event, matrix); break; case MotionEvent.ACTION_MOVE: if(updateTouchState) { evaluateTouchState(event, matrix); updateTouchState = false; } // Reuse the saved matrix matrix.set(savedMatrix); if (mode == DRAG) { if(translateEnabled) { // Get the start point PointF start = getStartPoint(0); int index = event.findPointerIndex(getId(0)); float dx = event.getX(index) - start.x; dx = corrector.correctRelative(Matrix.MTRANS_X, dx); float dy = event.getY(index) - start.y; dy = corrector.correctRelative(Matrix.MTRANS_Y, dy); matrix.postTranslate(dx, dy); } } else if (mode == PINCH) { // Get the new midpoint midPoint(mid, event, getId(0), getId(1)); // Rotate if(rotateEnabled) { float deg = startAngle - angle(event, getId(0), getId(1), startedLower(getStartPoint(0), getStartPoint(1))); matrix.postRotate(deg, mid.x, mid.y); } if(scaleEnabled) { // Scale float spacing = spacing(event, getId(0), getId(1)); float sx = spacing / startSpacing; sx = corrector.correctRelative(Matrix.MSCALE_X, sx); matrix.postScale(sx, sx, mid.x, mid.y); if(event.getHistorySize() > 0) { pinchVelocity = pinchVelocity(event, getId(0), getId(1), pinchVelocityWindow); } } if(dragOnPinchEnabled && translateEnabled) { // Translate float dx = mid.x - startMid.x; float dy = mid.y - startMid.y; matrix.postTranslate(dx, dy); } corrector.performAbsoluteCorrections(); } imageView.invalidate(); break; } return true; // indicate event was handled } /** * */ private class ImageGestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mode == DRAG) { if (flingDuration > 0 && !isAnimating()) { float factor = ((float) flingDuration / 1000f) * flingExaggeration; float[] values = corrector.getValues(); float dx = (velocityX * factor) * values[Matrix.MSCALE_X]; float dy = (velocityY * factor) * values[Matrix.MSCALE_Y]; PropertyValuesHolder flingX = PropertyValuesHolder.ofFloat(FlingAnimatorHandler.PROPERTY_TRANSLATE_X, values[Matrix.MTRANS_X], values[Matrix.MTRANS_X] + dx); PropertyValuesHolder flingY = PropertyValuesHolder.ofFloat(FlingAnimatorHandler.PROPERTY_TRANSLATE_Y, values[Matrix.MTRANS_Y], values[Matrix.MTRANS_Y] + dy); valueAnimator = ValueAnimator.ofPropertyValuesHolder(flingX, flingY); valueAnimator.setDuration(flingDuration); valueAnimator.addUpdateListener(new FlingAnimatorHandler(corrector)); valueAnimator.setInterpolator(new DecelerateInterpolator()); valueAnimator.start(); return true; } } return super.onFling(e1, e2, velocityX, velocityY); } @Override public boolean onDoubleTapEvent(MotionEvent e) { if (doubleTapZoomFactor > 0 && !isAnimating()) { float sx = corrector.getValues()[Matrix.MSCALE_X]; float innerFitScale = corrector.getInnerFitScale(); float reversalScale = innerFitScale * doubleTapZoomOutFactor; ScaleAnimatorHandler scaleAnimatorHandler = new ScaleAnimatorHandler(corrector, e.getX(), e.getY()); float scaleTo = sx > reversalScale ? innerFitScale : sx * doubleTapZoomFactor; animateZoom(sx, scaleTo, doubleTapZoomDuration, scaleAnimatorHandler, null); return true; } return super.onDoubleTap(e); } } /** * <p>Performs a zoom animation using the given <code>zoomFactor</code>.</p> * @param zoomFactor * @param duration */ public void animateZoom(float zoomFactor, long duration) { float sx = corrector.getValues()[Matrix.MSCALE_X]; animateZoom(sx, sx * zoomFactor, duration, new ScaleAnimatorHandler(corrector), null); } /** * <p>Performs a zoom animation using the given <code>zoomFactor</code> and centerpoint coordinates.</p> * @param zoomFactor * @param duration * @param x * @param y */ public void animateZoom(float zoomFactor, long duration, float x, float y) { animateZoom(zoomFactor, duration, x, y, null); } /** * <p>Performs a zoom animation using the given <code>zoomFactor</code> and centerpoint coordinates.</p> * @param zoomFactor * @param duration * @param x * @param y * @param interpolator */ public void animateZoom(float zoomFactor, long duration, float x, float y, Interpolator interpolator) { float sx = corrector.getValues()[Matrix.MSCALE_X]; animateZoom(sx, sx * zoomFactor, duration, new ScaleAnimatorHandler(corrector, x, y), interpolator); } /** * <p>Performs a zoom out animation so that the image entirely fits within the view.</p> * @param duration */ public void animateZoomOutToFit(long duration) { float sx = corrector.getValues()[Matrix.MSCALE_X]; animateZoom(sx, corrector.getInnerFitScale(), duration, new ScaleAnimatorHandler(corrector), null); } /** * <p>Performs a zoom out animation so that the image entirely fits within the view using centerpoint coordinates.</p> * @param duration * @param x * @param y */ public void animateZoomOutToFit(long duration, float x, float y) { float sx = corrector.getValues()[Matrix.MSCALE_X]; animateZoom(sx, corrector.getInnerFitScale(), duration, new ScaleAnimatorHandler(corrector, x, y), null); } /** * <p>Performs a zoom animation from <code>scaleFrom</code> to <code>scaleTo</code> using the given <code>ScaleAnimatorHandler</code>.</p> * @param scaleFrom * @param scaleTo * @param duration * @param scaleAnimatorHandler * @param interpolator */ private void animateZoom(float scaleFrom, float scaleTo, long duration, ScaleAnimatorHandler scaleAnimatorHandler, Interpolator interpolator) { if(isAnimating()) { throw new IllegalStateException("An animation is currently running; Check isAnimating() first!"); } valueAnimator = ValueAnimator.ofFloat(scaleFrom, scaleTo); valueAnimator.setDuration(duration); valueAnimator.addUpdateListener(scaleAnimatorHandler); if(interpolator != null) valueAnimator.setInterpolator(interpolator); valueAnimator.start(); } }