Java tutorial
/******************************************************************************* * Copyright 2011, 2012 Chris Banes. * <p> * 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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.frank.protean.photoview; import android.content.Context; import android.graphics.Matrix; import android.graphics.Matrix.ScaleToFit; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.v4.view.MotionEventCompat; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.View.OnLongClickListener; import android.view.ViewParent; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import android.widget.OverScroller; /** * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc. * It is made public in case you need to subclass something other than {@link ImageView} and still * gain the functionality that {@link PhotoView} offers */ public class PhotoViewAttacher implements View.OnTouchListener, View.OnLayoutChangeListener { private static float DEFAULT_MAX_SCALE = 3.0f; private static float DEFAULT_MID_SCALE = 1.75f; private static float DEFAULT_MIN_SCALE = 1.0f; private static int DEFAULT_ZOOM_DURATION = 200; private static final int EDGE_NONE = -1; private static final int EDGE_LEFT = 0; private static final int EDGE_RIGHT = 1; private static final int EDGE_BOTH = 2; private static int SINGLE_TOUCH = 1; private Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); private int mZoomDuration = DEFAULT_ZOOM_DURATION; 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 ImageView mImageView; // Gesture Detectors private GestureDetector mGestureDetector; private CustomGestureDetector 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 OnOutsidePhotoTapListener mOutsidePhotoTapListener; private OnViewTapListener mViewTapListener; private View.OnClickListener mOnClickListener; private OnLongClickListener mLongClickListener; private OnScaleChangedListener mScaleChangeListener; private OnSingleFlingListener mSingleFlingListener; private OnViewDragListener mOnViewDragListener; private FlingRunnable mCurrentFlingRunnable; private int mScrollEdge = EDGE_BOTH; private float mBaseRotation; private boolean mZoomEnabled = true; private ScaleType mScaleType = ScaleType.FIT_CENTER; private OnGestureListener onGestureListener = new OnGestureListener() { @Override public void onDrag(float dx, float dy) { if (mScaleDragDetector.isScaling()) { return; // Do not drag if we are already scaling } if (mOnViewDragListener != null) { mOnViewDragListener.onDrag(dx, dy); } 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 = mImageView.getParent(); if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f) || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } @Override public void onFling(float startX, float startY, float velocityX, float velocityY) { mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); mImageView.post(mCurrentFlingRunnable); } @Override public void onScale(float scaleFactor, float focusX, float focusY) { if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) { if (mScaleChangeListener != null) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); } mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); checkAndDisplayMatrix(); } } }; public PhotoViewAttacher(ImageView imageView) { mImageView = imageView; imageView.setOnTouchListener(this); imageView.addOnLayoutChangeListener(this); if (imageView.isInEditMode()) { return; } mBaseRotation = 0.0f; // Create Gesture Detectors... mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { // forward long click listener @Override public void onLongPress(MotionEvent e) { if (mLongClickListener != null) { mLongClickListener.onLongClick(mImageView); } } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mSingleFlingListener != null) { if (getScale() > DEFAULT_MIN_SCALE) { return false; } if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH || MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) { return false; } return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); } return false; } }); mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { @Override public boolean onSingleTapConfirmed(MotionEvent e) { if (mOnClickListener != null) { mOnClickListener.onClick(mImageView); } final RectF displayRect = getDisplayRect(); final float x = e.getX(), y = e.getY(); if (mViewTapListener != null) { mViewTapListener.onViewTap(mImageView, x, y); } if (displayRect != null) { // 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(); if (mPhotoTapListener != null) { mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); } return true; } else { if (mOutsidePhotoTapListener != null) { mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); } } } return false; } @Override public boolean onDoubleTap(MotionEvent ev) { try { float scale = getScale(); float x = ev.getX(); float y = ev.getY(); if (scale < getMediumScale()) { setScale(getMediumScale(), x, y, true); } else if (scale >= getMediumScale() && scale < getMaximumScale()) { setScale(getMaximumScale(), x, y, true); } else { setScale(getMinimumScale(), x, y, true); } } 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; } }); } public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); } public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { this.mScaleChangeListener = onScaleChangeListener; } public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { this.mSingleFlingListener = onSingleFlingListener; } @Deprecated public boolean isZoomEnabled() { return mZoomEnabled; } public RectF getDisplayRect() { checkMatrixBounds(); return getDisplayRect(getDrawMatrix()); } public boolean setDisplayMatrix(Matrix finalMatrix) { if (finalMatrix == null) { throw new IllegalArgumentException("Matrix cannot be null"); } if (mImageView.getDrawable() == null) { return false; } mSuppMatrix.set(finalMatrix); checkAndDisplayMatrix(); return true; } public void setBaseRotation(final float degrees) { mBaseRotation = degrees % 360; update(); setRotationBy(mBaseRotation); checkAndDisplayMatrix(); } public void setRotationTo(float degrees) { mSuppMatrix.setRotate(degrees % 360); checkAndDisplayMatrix(); } public void setRotationBy(float degrees) { mSuppMatrix.postRotate(degrees % 360); checkAndDisplayMatrix(); } 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; } @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { // Update our base matrix, as the bounds have changed if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { updateBaseMatrix(mImageView.getDrawable()); } } @Override public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: ViewParent parent = v.getParent(); // First, disable the Parent from intercepting the touch // event if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } // If we're flinging, and the user presses down, cancel // fling cancelFling(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // If the user has zoomed less than min scale, zoom back // to min scale if (getScale() < mMinScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY())); handled = true; } } else if (getScale() > mMaxScale) { RectF rect = getDisplayRect(); if (rect != null) { v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, rect.centerX(), rect.centerY())); handled = true; } } break; } // Try the Scale/Drag detector if (mScaleDragDetector != null) { boolean wasScaling = mScaleDragDetector.isScaling(); boolean wasDragging = mScaleDragDetector.isDragging(); handled = mScaleDragDetector.onTouchEvent(ev); boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); mBlockParentIntercept = didntScale && didntDrag; } // Check to see if the user double tapped if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { handled = true; } } return handled; } public void setAllowParentInterceptOnEdge(boolean allow) { mAllowParentInterceptOnEdge = allow; } public void setMinimumScale(float minimumScale) { Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); mMinScale = minimumScale; } public void setMediumScale(float mediumScale) { Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); mMidScale = mediumScale; } public void setMaximumScale(float maximumScale) { Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); mMaxScale = maximumScale; } public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); mMinScale = minimumScale; mMidScale = mediumScale; mMaxScale = maximumScale; } public void setOnLongClickListener(OnLongClickListener listener) { mLongClickListener = listener; } public void setOnClickListener(View.OnClickListener listener) { mOnClickListener = listener; } public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { mMatrixChangeListener = listener; } public void setOnPhotoTapListener(OnPhotoTapListener listener) { mPhotoTapListener = listener; } public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; } public void setOnViewTapListener(OnViewTapListener listener) { mViewTapListener = listener; } public void setOnViewDragListener(OnViewDragListener listener) { mOnViewDragListener = listener; } public void setScale(float scale) { setScale(scale, false); } public void setScale(float scale, boolean animate) { setScale(scale, (mImageView.getRight()) / 2, (mImageView.getBottom()) / 2, animate); } public void setScale(float scale, float focalX, float focalY, boolean animate) { // Check to see if the scale is within bounds if (scale < mMinScale || scale > mMaxScale) { throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale"); } if (animate) { mImageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY)); } else { mSuppMatrix.setScale(scale, scale, focalX, focalY); checkAndDisplayMatrix(); } } /** * Set the zoom interpolator * * @param interpolator the zoom interpolator */ public void setZoomInterpolator(Interpolator interpolator) { mInterpolator = interpolator; } public void setScaleType(ScaleType scaleType) { if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { mScaleType = scaleType; update(); } } public boolean isZoomable() { return mZoomEnabled; } public void setZoomable(boolean zoomable) { mZoomEnabled = zoomable; update(); } public void update() { if (mZoomEnabled) { // Update the base matrix using the current drawable updateBaseMatrix(mImageView.getDrawable()); } else { // Reset the Matrix... resetMatrix(); } } /** * Get the display matrix * * @param matrix target matrix to copy to */ public void getDisplayMatrix(Matrix matrix) { matrix.set(getDrawMatrix()); } /** * Get the current support matrix */ public void getSuppMatrix(Matrix matrix) { matrix.set(mSuppMatrix); } private Matrix getDrawMatrix() { mDrawMatrix.set(mBaseMatrix); mDrawMatrix.postConcat(mSuppMatrix); return mDrawMatrix; } public Matrix getImageMatrix() { return mDrawMatrix; } public void setZoomTransitionDuration(int milliseconds) { this.mZoomDuration = 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 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 its contents */ private void resetMatrix() { mSuppMatrix.reset(); setRotationBy(mBaseRotation); setImageViewMatrix(getDrawMatrix()); checkMatrixBounds(); } private void setImageViewMatrix(Matrix matrix) { mImageView.setImageMatrix(matrix); // Call MatrixChangedListener if needed if (mMatrixChangeListener != null) { RectF displayRect = getDisplayRect(matrix); if (displayRect != null) { mMatrixChangeListener.onMatrixChanged(displayRect); } } } /** * Helper method that simply checks the Matrix, and then displays the result */ private void checkAndDisplayMatrix() { if (checkMatrixBounds()) { setImageViewMatrix(getDrawMatrix()); } } /** * 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) { Drawable d = mImageView.getDrawable(); if (d != null) { mDisplayRect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); matrix.mapRect(mDisplayRect); return mDisplayRect; } return null; } /** * Calculate Matrix for FIT_CENTER * * @param drawable - Drawable being displayed */ private void updateBaseMatrix(Drawable drawable) { if (drawable == null) { return; } final float viewWidth = getImageViewWidth(mImageView); final float viewHeight = getImageViewHeight(mImageView); final int drawableWidth = drawable.getIntrinsicWidth(); final int drawableHeight = drawable.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); if ((int) mBaseRotation % 180 != 0) { mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); } 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 boolean checkMatrixBounds() { final RectF rect = getDisplayRect(getDrawMatrix()); if (rect == null) { return false; } final float height = rect.height(), width = rect.width(); float deltaX = 0, deltaY = 0; final int viewHeight = getImageViewHeight(mImageView); 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(mImageView); 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; } private int getImageViewWidth(ImageView imageView) { return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); } private int getImageViewHeight(ImageView imageView) { return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); } private void cancelFling() { if (mCurrentFlingRunnable != null) { mCurrentFlingRunnable.cancelFling(); mCurrentFlingRunnable = null; } } 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() { float t = interpolate(); float scale = mZoomStart + t * (mZoomEnd - mZoomStart); float deltaScale = scale / getScale(); onGestureListener.onScale(deltaScale, mFocalX, mFocalY); // We haven't hit our target scale yet, so post ourselves again if (t < 1f) { Compat.postOnAnimation(mImageView, this); } } private float interpolate() { float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; t = Math.min(1f, t); t = mInterpolator.getInterpolation(t); return t; } } private class FlingRunnable implements Runnable { private final OverScroller mScroller; private int mCurrentX, mCurrentY; public FlingRunnable(Context context) { mScroller = new OverScroller(context); } public void cancelFling() { mScroller.forceFinished(true); } public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) { final RectF rect = getDisplayRect(); if (rect == null) { 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 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 } if (mScroller.computeScrollOffset()) { final int newX = mScroller.getCurrX(); final int newY = mScroller.getCurrY(); mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); checkAndDisplayMatrix(); mCurrentX = newX; mCurrentY = newY; // Post On animation Compat.postOnAnimation(mImageView, this); } } } }