/******************************************************************************* * Copyright 2011, 2012 Chris Banes. * Copyright (C) 2015 The CyanogenMod Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/ package com.android.settings.contributors; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Matrix; import android.graphics.Matrix.ScaleToFit; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.VelocityTracker; import android.view.View; import android.view.ScaleGestureDetector.OnScaleGestureListener; import android.view.View.OnLongClickListener; import android.view.ViewConfiguration; import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.OverScroller; import java.lang.ref.WeakReference; import static android.view.MotionEvent.ACTION_CANCEL; import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_UP; public class ContributorsCloudViewController implements View.OnTouchListener, ViewTreeObserver.OnGlobalLayoutListener { private static final String LOG_TAG = "ContributorsCloud"; public static final float DEFAULT_MAX_SCALE = 3.0f; public static final float DEFAULT_MID_SCALE = 1.75f; public static final float DEFAULT_MIN_SCALE = 1.0f; public static final int DEFAULT_ZOOM_DURATION = 200; // let debug flag be dynamic, but still Proguard can be used to remove from // release builds private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG); static final Interpolator sInterpolator = new AccelerateDecelerateInterpolator(); int ZOOM_DURATION = DEFAULT_ZOOM_DURATION; static final int EDGE_NONE = -1; static final int EDGE_LEFT = 0; static final int EDGE_RIGHT = 1; static final int EDGE_BOTH = 2; private float mMinScale = DEFAULT_MIN_SCALE; private float mMidScale = DEFAULT_MID_SCALE; private float mMaxScale = DEFAULT_MAX_SCALE; private boolean mAllowParentInterceptOnEdge = true; private boolean mBlockParentIntercept = false; private static final int INVALID_POINTER_ID = -1; private int mActivePointerId = INVALID_POINTER_ID; private int mActivePointerIndex = 0; private float mLastTouchX; private float mLastTouchY; private final float mTouchSlop; private final float mMinimumVelocity; private VelocityTracker mVelocityTracker; private boolean mIsDragging; private boolean mIgnoreDoubleTapScale; private static void checkZoomLevels(float minZoom, float midZoom, float maxZoom) { if (minZoom >= midZoom) { throw new IllegalArgumentException( "MinZoom has to be less than MidZoom"); } else if (midZoom >= maxZoom) { throw new IllegalArgumentException( "MidZoom has to be less than MaxZoom"); } } /** * @return true if the ImageView exists, and it's Drawable existss */ private static boolean hasDrawable(ImageView imageView) { return null != imageView && null != imageView.getDrawable(); } /** * @return true if the ScaleType is supported. */ private static boolean isSupportedScaleType(final ScaleType scaleType) { if (null == scaleType) { return false; } switch (scaleType) { case MATRIX: throw new IllegalArgumentException(scaleType.name() + " is not supported in PhotoView"); default: return true; } } /** * Set's the ImageView's ScaleType to Matrix. */ private static void setImageViewScaleTypeMatrix(ImageView imageView) { /** * PhotoView sets it's own ScaleType to Matrix, then diverts all calls * setScaleType to this.setScaleType automatically. */ if (null != imageView /*&& !(imageView instanceof IPhotoView)*/) { if (!ScaleType.MATRIX.equals(imageView.getScaleType())) { imageView.setScaleType(ScaleType.MATRIX); } } } public class DefaultOnDoubleTapListener implements GestureDetector.OnDoubleTapListener { private ContributorsCloudViewController controller; public DefaultOnDoubleTapListener(ContributorsCloudViewController controller) { setController(controller); } public void setController(ContributorsCloudViewController controller) { this.controller = controller; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (controller == null) return false; ImageView imageView = controller.getImageView(); if (null != controller.getOnPhotoTapListener()) { final RectF displayRect = controller.getDisplayRect(); if (null != displayRect) { final float x = e.getX(), y = e.getY(); // Check to see if the user tapped on the photo if (displayRect.contains(x, y)) { float xResult = (x - displayRect.left) / displayRect.width(); float yResult = (y - displayRect.top) / displayRect.height(); controller.getOnPhotoTapListener().onPhotoTap(imageView, xResult, yResult); return true; } } } if (null != controller.getOnViewTapListener()) { controller.getOnViewTapListener().onViewTap(imageView, e.getX(), e.getY()); } return false; } @Override public boolean onDoubleTap(MotionEvent ev) { if (controller == null) return false; try { float scale = controller.getScale(); float x = ev.getX(); float y = ev.getY(); if (!mIgnoreDoubleTapScale && scale < controller.getMediumScale()) { controller.setScale(controller.getMediumScale(), x, y, true); } else if (!mIgnoreDoubleTapScale && scale >= controller.getMediumScale() && scale < controller.getMaximumScale()) { controller.setScale(controller.getMaximumScale(), x, y, true); } else { controller.setScale(controller.getMinimumScale(), x, y, true); } mIgnoreDoubleTapScale = false; } catch (ArrayIndexOutOfBoundsException e) { // Can sometimes happen when getX() and getY() is called } return true; } @Override public boolean onDoubleTapEvent(MotionEvent e) { // Wait for the confirmed onDoubleTap() instead return false; } } private WeakReference mImageView; // Gesture Detectors private GestureDetector mGestureDetector; private ScaleGestureDetector mScaleDragDetector; // These are set so we don't keep allocating them on the heap private final Matrix mBaseMatrix = new Matrix(); private final Matrix mDrawMatrix = new Matrix(); private final Matrix mSuppMatrix = new Matrix(); private final RectF mDisplayRect = new RectF(); private final float[] mMatrixValues = new float[9]; // Listeners private OnMatrixChangedListener mMatrixChangeListener; private OnPhotoTapListener mPhotoTapListener; private OnViewTapListener mViewTapListener; private OnLongClickListener mLongClickListener; private OnScaleChangeListener mScaleChangeListener; private int mIvTop, mIvRight, mIvBottom, mIvLeft; private FlingRunnable mCurrentFlingRunnable; private int mScrollEdge = EDGE_BOTH; private boolean mZoomEnabled; private ScaleType mScaleType = ScaleType.FIT_CENTER; public ContributorsCloudViewController(ImageView imageView) { this(imageView, true); } public ContributorsCloudViewController(ImageView imageView, boolean zoomable) { final ViewConfiguration configuration = ViewConfiguration.get(imageView.getContext()); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mTouchSlop = configuration.getScaledTouchSlop(); mImageView = new WeakReference<>(imageView); imageView.setDrawingCacheEnabled(true); imageView.setOnTouchListener(this); ViewTreeObserver observer = imageView.getViewTreeObserver(); if (null != observer) observer.addOnGlobalLayoutListener(this); // Make sure we using MATRIX Scale Type setImageViewScaleTypeMatrix(imageView); if (imageView.isInEditMode()) { return; } // Create Gesture Detectors... OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { @Override public boolean onScale(ScaleGestureDetector detector) { float scaleFactor = detector.getScaleFactor(); if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) return false; ContributorsCloudViewController.this.onScale(scaleFactor, detector.getFocusX(), detector.getFocusY()); return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { // NO-OP } }; mScaleDragDetector = new ScaleGestureDetector(imageView.getContext(), mScaleListener); mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { // forward long click listener @Override public void onLongPress(MotionEvent e) { if (null != mLongClickListener) { mLongClickListener.onLongClick(getImageView()); } } }); mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); // Finally, update the UI so that we're zoomable setZoomable(zoomable); } public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { if (newOnDoubleTapListener != null) { this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); } else { this.mGestureDetector.setOnDoubleTapListener(new DefaultOnDoubleTapListener(this)); } } public void setOnScaleChangeListener(OnScaleChangeListener onScaleChangeListener) { this.mScaleChangeListener = onScaleChangeListener; } public boolean canZoom() { return mZoomEnabled; } /** * Clean-up the resources attached to this object. This needs to be called when the ImageView is * no longer used. A good example is from {@link android.view.View#onDetachedFromWindow()} or * from {@link android.app.Activity#onDestroy()}. This is automatically called if you are using * {@link uk.co.senab.photoview.PhotoView}. */ @SuppressWarnings("deprecation") public void cleanup() { if (null == mImageView) { return; // cleanup already done } final ImageView imageView = mImageView.get(); if (null != imageView) { // Remove this as a global layout listener ViewTreeObserver observer = imageView.getViewTreeObserver(); if (null != observer && observer.isAlive()) { observer.removeGlobalOnLayoutListener(this); } // Remove the ImageView's reference to this imageView.setOnTouchListener(null); // make sure a pending fling runnable won't be run cancelFling(); } if (null != mGestureDetector) { mGestureDetector.setOnDoubleTapListener(null); } // Clear listeners too mMatrixChangeListener = null; mPhotoTapListener = null; mViewTapListener = null; // Finally, clear ImageView mImageView = null; } public RectF getDisplayRect() { checkMatrixBounds(); return getDisplayRect(getDrawMatrix()); } public boolean setDisplayMatrix(Matrix finalMatrix) { if (finalMatrix == null) throw new IllegalArgumentException("Matrix cannot be null"); ImageView imageView = getImageView(); if (null == imageView) return false; if (null == imageView.getDrawable()) return false; mSuppMatrix.set(finalMatrix); setImageViewMatrix(getDrawMatrix()); checkMatrixBounds(); return true; } public void setRotationTo(float degrees) { mSuppMatrix.setRotate(degrees % 360); checkAndDisplayMatrix(); } public void setRotationBy(float degrees) { mSuppMatrix.postRotate(degrees % 360); checkAndDisplayMatrix(); } public ImageView getImageView() { ImageView imageView = null; if (null != mImageView) { imageView = mImageView.get(); } // If we don't have an ImageView, call cleanup() if (null == imageView) { cleanup(); Log.i(LOG_TAG, "ImageView no longer exists. You should " + "not use this reference any more."); } return imageView; } public float getMinimumScale() { return mMinScale; } public float getMediumScale() { return mMidScale; } public float getMaximumScale() { return mMaxScale; } public float getScale() { return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); } public ScaleType getScaleType() { return mScaleType; } public void onDrag(float dx, float dy) { if (mScaleDragDetector.isInProgress()) { return; // Do not drag if we are already scaling } if (DEBUG) { Log.d(LOG_TAG, String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy)); } ImageView imageView = getImageView(); mSuppMatrix.postTranslate(dx, dy); checkAndDisplayMatrix(); /** * Here we decide whether to let the ImageView's parent to start taking * over the touch event. * * First we check whether this function is enabled. We never want the * parent to take over if we're scaling. We then check the edge we're * on, and the direction of the scroll (i.e. if we're pulling against * the edge, aka 'overscrolling', let the parent take over). */ ViewParent parent = imageView.getParent(); if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isInProgress() && !mBlockParentIntercept) { if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f) || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { if (null != parent) parent.requestDisallowInterceptTouchEvent(false); } } else { if (null != parent) { parent.requestDisallowInterceptTouchEvent(true); } } mIgnoreDoubleTapScale = false; } public void onFling(float startX, float startY, float velocityX, float velocityY) { if (DEBUG) { Log.d(LOG_TAG, "onFling. sX: " + startX + " sY: " + startY + " Vx: " + velocityX + " Vy: " + velocityY); } ImageView imageView = getImageView(); mCurrentFlingRunnable = new FlingRunnable(imageView.getContext()); mCurrentFlingRunnable.fling(getImageViewWidth(imageView), getImageViewHeight(imageView), (int) velocityX, (int) velocityY); imageView.post(mCurrentFlingRunnable); mIgnoreDoubleTapScale = false; } @Override public void onGlobalLayout() { ImageView imageView = getImageView(); if (null != imageView) { if (mZoomEnabled) { final int top = imageView.getTop(); final int right = imageView.getRight(); final int bottom = imageView.getBottom(); final int left = imageView.getLeft(); /** * We need to check whether the ImageView's bounds have changed. * This would be easier if we targeted API 11+ as we could just use * View.OnLayoutChangeListener. Instead we have to replicate the * work, keeping track of the ImageView's bounds and then checking * if the values change. */ if (top != mIvTop || bottom != mIvBottom || left != mIvLeft || right != mIvRight) { // Update our base matrix, as the bounds have changed updateBaseMatrix(imageView.getDrawable()); // Update values as something has changed mIvTop = top; mIvRight = right; mIvBottom = bottom; mIvLeft = left; } } else { updateBaseMatrix(imageView.getDrawable()); } } } public void onScale(float scaleFactor, float focusX, float focusY) { if (DEBUG) { Log.d(LOG_TAG,String.format("onScale: scale: %.2f. fX: %.2f. fY: %.2f", scaleFactor, focusX, focusY)); } if (getScale() < mMaxScale || scaleFactor < 1f) { if (null != mScaleChangeListener) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); } mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); checkAndDisplayMatrix(); } } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; if (mZoomEnabled && hasDrawable((ImageView) v)) { ViewParent parent = v.getParent(); switch (ev.getAction()) { case ACTION_DOWN: // First, disable the Parent from intercepting the touch // event if (null != parent) { parent.requestDisallowInterceptTouchEvent(true); } else { Log.i(LOG_TAG, "onTouch getParent() returned null"); } // If we're flinging, and the user presses down, cancel // fling cancelFling(); break; case ACTION_CANCEL: case ACTION_UP: // If the user has zoomed less than min scale, zoom back // to min scale if (getScale() < mMinScale) { RectF rect = getDisplayRect(); if (null != rect) { v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY())); handled = true; } } break; } // Try the Scale/Drag detector if (null != mScaleDragDetector) { boolean wasScaling = mScaleDragDetector.isInProgress(); boolean wasDragging = mIsDragging; handled = onTouchEvent(ev); boolean didntScale = !wasScaling && !mScaleDragDetector.isInProgress(); boolean didntDrag = !wasDragging && !mIsDragging; mBlockParentIntercept = didntScale && didntDrag; } // Check to see if the user double tapped if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) { handled = true; } } return handled; } public void setAllowParentInterceptOnEdge(boolean allow) { mAllowParentInterceptOnEdge = allow; } public void setMinimumScale(float minimumScale) { checkZoomLevels(minimumScale, mMidScale, mMaxScale); mMinScale = minimumScale; } public void setMediumScale(float mediumScale) { checkZoomLevels(mMinScale, mediumScale, mMaxScale); mMidScale = mediumScale; } public void setMaximumScale(float maximumScale) { checkZoomLevels(mMinScale, mMidScale, maximumScale); mMaxScale = maximumScale; } public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { checkZoomLevels(minimumScale, mediumScale, maximumScale); mMinScale = minimumScale; mMidScale = mediumScale; mMaxScale = maximumScale; } public void setOnLongClickListener(OnLongClickListener listener) { mLongClickListener = listener; } public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { mMatrixChangeListener = listener; } public void setOnPhotoTapListener(OnPhotoTapListener listener) { mPhotoTapListener = listener; } public OnPhotoTapListener getOnPhotoTapListener() { return mPhotoTapListener; } public void setOnViewTapListener(OnViewTapListener listener) { mViewTapListener = listener; } public OnViewTapListener getOnViewTapListener() { return mViewTapListener; } public void setScale(float scale) { setScale(scale, false); } public void setScale(float scale, boolean animate) { ImageView imageView = getImageView(); if (null != imageView) { setScale(scale, (imageView.getRight()) / 2, (imageView.getBottom()) / 2, animate); } } public void setScale(float scale, float focalX, float focalY, boolean animate) { ImageView imageView = getImageView(); if (null != imageView) { // Check to see if the scale is within bounds if (scale < mMinScale || scale > mMaxScale) { Log.i(LOG_TAG, "Scale must be within the range of minScale and maxScale"); return; } if (animate) { imageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY)); } else { mSuppMatrix.setScale(scale, scale, focalX, focalY); checkAndDisplayMatrix(); } // This focuses to some point of the view, so treat it as a return point to // minimum zoom if (scale < getMediumScale()) { mIgnoreDoubleTapScale = true; } } } public void setScaleType(ScaleType scaleType) { if (isSupportedScaleType(scaleType) && scaleType != mScaleType) { mScaleType = scaleType; // Finally update update(); } } public void setZoomable(boolean zoomable) { mZoomEnabled = zoomable; update(); } public void update() { ImageView imageView = getImageView(); if (null != imageView) { if (mZoomEnabled) { // Make sure we using MATRIX Scale Type setImageViewScaleTypeMatrix(imageView); // Update the base matrix using the current drawable updateBaseMatrix(imageView.getDrawable()); } else { // Reset the Matrix... resetMatrix(); } } } public Matrix getDisplayMatrix() { return new Matrix(getDrawMatrix()); } public Matrix getDrawMatrix() { mDrawMatrix.set(mBaseMatrix); mDrawMatrix.postConcat(mSuppMatrix); return mDrawMatrix; } private void cancelFling() { if (null != mCurrentFlingRunnable) { mCurrentFlingRunnable.cancelFling(); mCurrentFlingRunnable = null; } } /** * Helper method that simply checks the Matrix, and then displays the result */ private void checkAndDisplayMatrix() { if (checkMatrixBounds()) { setImageViewMatrix(getDrawMatrix()); } } private void checkImageViewScaleType() { ImageView imageView = getImageView(); /** * PhotoView's getScaleType() will just divert to this.getScaleType() so * only call if we're not attached to a PhotoView. */ if (null != imageView/* && !(imageView instanceof IPhotoView)*/) { if (!ScaleType.MATRIX.equals(imageView.getScaleType())) { throw new IllegalStateException("The ImageView's ScaleType has been " + "changed since attaching to this controller"); } } } private boolean checkMatrixBounds() { final ImageView imageView = getImageView(); if (null == imageView) { return false; } final RectF rect = getDisplayRect(getDrawMatrix()); if (null == rect) { return false; } final float height = rect.height(), width = rect.width(); float deltaX = 0, deltaY = 0; final int viewHeight = getImageViewHeight(imageView); if (height <= viewHeight) { switch (mScaleType) { case FIT_START: deltaY = -rect.top; break; case FIT_END: deltaY = viewHeight - height - rect.top; break; default: deltaY = (viewHeight - height) / 2 - rect.top; break; } } else if (rect.top > 0) { deltaY = -rect.top; } else if (rect.bottom < viewHeight) { deltaY = viewHeight - rect.bottom; } final int viewWidth = getImageViewWidth(imageView); if (width <= viewWidth) { switch (mScaleType) { case FIT_START: deltaX = -rect.left; break; case FIT_END: deltaX = viewWidth - width - rect.left; break; default: deltaX = (viewWidth - width) / 2 - rect.left; break; } mScrollEdge = EDGE_BOTH; } else if (rect.left > 0) { mScrollEdge = EDGE_LEFT; deltaX = -rect.left; } else if (rect.right < viewWidth) { deltaX = viewWidth - rect.right; mScrollEdge = EDGE_RIGHT; } else { mScrollEdge = EDGE_NONE; } // Finally actually translate the matrix mSuppMatrix.postTranslate(deltaX, deltaY); return true; } /** * Helper method that maps the supplied Matrix to the current Drawable * * @param matrix - Matrix to map Drawable against * @return RectF - Displayed Rectangle */ private RectF getDisplayRect(Matrix matrix) { ImageView imageView = getImageView(); if (null != imageView) { Drawable d = imageView.getDrawable(); if (null != d) { mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); matrix.mapRect(mDisplayRect); return mDisplayRect; } } return null; } public Bitmap getVisibleRectangleBitmap() { ImageView imageView = getImageView(); return imageView == null ? null : imageView.getDrawingCache(); } public void setZoomTransitionDuration(int milliseconds) { if (milliseconds < 0) milliseconds = DEFAULT_ZOOM_DURATION; this.ZOOM_DURATION = milliseconds; } /** * Helper method that 'unpacks' a Matrix and returns the required value * * @param matrix - Matrix to unpack * @param whichValue - Which value from Matrix.M* to return * @return float - returned value */ private float getValue(Matrix matrix, int whichValue) { matrix.getValues(mMatrixValues); return mMatrixValues[whichValue]; } /** * Resets the Matrix back to FIT_CENTER, and then displays it.s */ private void resetMatrix() { mSuppMatrix.reset(); setImageViewMatrix(getDrawMatrix()); checkMatrixBounds(); } private void setImageViewMatrix(Matrix matrix) { ImageView imageView = getImageView(); if (null != imageView) { checkImageViewScaleType(); imageView.setImageMatrix(matrix); // Call MatrixChangedListener if needed if (null != mMatrixChangeListener) { RectF displayRect = getDisplayRect(matrix); if (null != displayRect) { mMatrixChangeListener.onMatrixChanged(displayRect); } } } } /** * Calculate Matrix for FIT_CENTER * * @param d - Drawable being displayed */ private void updateBaseMatrix(Drawable d) { ImageView imageView = getImageView(); if (null == imageView || null == d) { return; } final float viewWidth = getImageViewWidth(imageView); final float viewHeight = getImageViewHeight(imageView); final int drawableWidth = d.getIntrinsicWidth(); final int drawableHeight = d.getIntrinsicHeight(); mBaseMatrix.reset(); final float widthScale = viewWidth / drawableWidth; final float heightScale = viewHeight / drawableHeight; if (mScaleType == ScaleType.CENTER) { mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, (viewHeight - drawableHeight) / 2F); } else if (mScaleType == ScaleType.CENTER_CROP) { float scale = Math.max(widthScale, heightScale); mBaseMatrix.postScale(scale, scale); mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F); } else if (mScaleType == ScaleType.CENTER_INSIDE) { float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); mBaseMatrix.postScale(scale, scale); mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, (viewHeight - drawableHeight * scale) / 2F); } else { RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); switch (mScaleType) { case FIT_CENTER: mBaseMatrix .setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); break; case FIT_START: mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); break; case FIT_END: mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); break; case FIT_XY: mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); break; default: break; } } resetMatrix(); } private int getImageViewWidth(ImageView imageView) { if (null == imageView) return 0; return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); } private int getImageViewHeight(ImageView imageView) { if (null == imageView) return 0; return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); } private float getActiveX(MotionEvent ev) { try { return ev.getX(mActivePointerIndex); } catch (Exception e) { return ev.getX(); } } private float getActiveY(MotionEvent ev) { try { return ev.getY(mActivePointerIndex); } catch (Exception e) { return ev.getY(); } } public boolean onTouchEvent(MotionEvent ev) { mScaleDragDetector.onTouchEvent(ev); final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: mActivePointerId = ev.getPointerId(0); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mActivePointerId = INVALID_POINTER_ID; break; case MotionEvent.ACTION_POINTER_UP: // Ignore deprecation, ACTION_POINTER_ID_MASK and // ACTION_POINTER_ID_SHIFT has same value and are deprecated // You can have either deprecation or lint target api warning final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; final int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); mLastTouchX = ev.getX(newPointerIndex); mLastTouchY = ev.getY(newPointerIndex); } break; } mActivePointerIndex = ev.findPointerIndex( mActivePointerId != INVALID_POINTER_ID ? mActivePointerId : 0); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { mVelocityTracker = VelocityTracker.obtain(); if (null != mVelocityTracker) { mVelocityTracker.addMovement(ev); } else { Log.i(LOG_TAG, "Velocity tracker is null"); } mLastTouchX = getActiveX(ev); mLastTouchY = getActiveY(ev); mIsDragging = false; break; } case MotionEvent.ACTION_MOVE: { final float x = getActiveX(ev); final float y = getActiveY(ev); final float dx = x - mLastTouchX, dy = y - mLastTouchY; if (!mIsDragging) { // Use Pythagoras to see if drag length is larger than // touch slop mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; } if (mIsDragging) { onDrag(dx, dy); mLastTouchX = x; mLastTouchY = y; if (null != mVelocityTracker) { mVelocityTracker.addMovement(ev); } } break; } case MotionEvent.ACTION_CANCEL: { // Recycle Velocity Tracker if (null != mVelocityTracker) { mVelocityTracker.recycle(); mVelocityTracker = null; } break; } case MotionEvent.ACTION_UP: { if (mIsDragging) { if (null != mVelocityTracker) { mLastTouchX = getActiveX(ev); mLastTouchY = getActiveY(ev); // Compute velocity within the last 1000ms mVelocityTracker.addMovement(ev); mVelocityTracker.computeCurrentVelocity(1000); final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker .getYVelocity(); // If the velocity is greater than minVelocity, call // listener if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { onFling(mLastTouchX, mLastTouchY, -vX, -vY); } } } // Recycle Velocity Tracker if (null != mVelocityTracker) { mVelocityTracker.recycle(); mVelocityTracker = null; } break; } } return true; } /** * Interface definition for a callback to be invoked when the internal Matrix has changed for * this View. * * @author Chris Banes */ public static interface OnMatrixChangedListener { /** * Callback for when the Matrix displaying the Drawable has changed. This could be because * the View's bounds have changed, or the user has zoomed. * * @param rect - Rectangle displaying the Drawable's new bounds. */ void onMatrixChanged(RectF rect); } /** * Interface definition for callback to be invoked when attached ImageView scale changes * * @author Marek Sebera */ public static interface OnScaleChangeListener { /** * Callback for when the scale changes * * @param scaleFactor the scale factor (<1 for zoom out, >1 for zoom in) * @param focusX focal point X position * @param focusY focal point Y position */ void onScaleChange(float scaleFactor, float focusX, float focusY); } /** * Interface definition for a callback to be invoked when the Photo is tapped with a single * tap. * * @author Chris Banes */ public static interface OnPhotoTapListener { /** * A callback to receive where the user taps on a photo. You will only receive a callback if * the user taps on the actual photo, tapping on 'whitespace' will be ignored. * * @param view - View the user tapped. * @param x - where the user tapped from the of the Drawable, as percentage of the * Drawable width. * @param y - where the user tapped from the top of the Drawable, as percentage of the * Drawable height. */ void onPhotoTap(View view, float x, float y); } /** * Interface definition for a callback to be invoked when the ImageView is tapped with a single * tap. * * @author Chris Banes */ public static interface OnViewTapListener { /** * A callback to receive where the user taps on a ImageView. You will receive a callback if * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. * * @param view - View the user tapped. * @param x - where the user tapped from the left of the View. * @param y - where the user tapped from the top of the View. */ void onViewTap(View view, float x, float y); } private class AnimatedZoomRunnable implements Runnable { private final float mFocalX, mFocalY; private final long mStartTime; private final float mZoomStart, mZoomEnd; public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, final float focalY) { mFocalX = focalX; mFocalY = focalY; mStartTime = System.currentTimeMillis(); mZoomStart = currentZoom; mZoomEnd = targetZoom; } @Override public void run() { ImageView imageView = getImageView(); if (imageView == null) { return; } float t = interpolate(); float scale = mZoomStart + t * (mZoomEnd - mZoomStart); float deltaScale = scale / getScale(); onScale(deltaScale, mFocalX, mFocalY); // We haven't hit our target scale yet, so post ourselves again if (t < 1f) { imageView.postOnAnimation(this); } } private float interpolate() { float t = 1f * (System.currentTimeMillis() - mStartTime) / ZOOM_DURATION; t = Math.min(1f, t); t = sInterpolator.getInterpolation(t); return t; } } private class FlingRunnable implements Runnable { protected final OverScroller mScroller; private int mCurrentX, mCurrentY; public FlingRunnable(Context context) { mScroller = new OverScroller(context); } public void cancelFling() { if (DEBUG) { Log.d(LOG_TAG, "Cancel Fling"); } mScroller.forceFinished(true); } public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) { final RectF rect = getDisplayRect(); if (null == rect) { return; } final int startX = Math.round(-rect.left); final int minX, maxX, minY, maxY; if (viewWidth < rect.width()) { minX = 0; maxX = Math.round(rect.width() - viewWidth); } else { minX = maxX = startX; } final int startY = Math.round(-rect.top); if (viewHeight < rect.height()) { minY = 0; maxY = Math.round(rect.height() - viewHeight); } else { minY = maxY = startY; } mCurrentX = startX; mCurrentY = startY; if (DEBUG) { Log.d(LOG_TAG, "fling. StartX:" + startX + " StartY:" + startY + " MaxX:" + maxX + " MaxY:" + maxY); } // If we actually can move, fling the scroller if (startX != maxX || startY != maxY) { mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0); } } @Override public void run() { if (mScroller.isFinished()) { return; // remaining post that should not be handled } ImageView imageView = getImageView(); if (null != imageView && mScroller.computeScrollOffset()) { final int newX = mScroller.getCurrX(); final int newY = mScroller.getCurrY(); if (DEBUG) { Log.d(LOG_TAG, "fling run(). CurrentX:" + mCurrentX + " CurrentY:" + mCurrentY + " NewX:" + newX + " NewY:" + newY); } mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); setImageViewMatrix(getDrawMatrix()); mCurrentX = newX; mCurrentY = newY; // Post On animation imageView.postOnAnimation(this); } } } }