package org.aisen.weibo.sina.ui.widget.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.util.Log; import android.view.MotionEvent; import android.view.View.OnLongClickListener; import android.view.ViewTreeObserver; import android.widget.ImageView; import android.widget.ImageView.ScaleType; import org.aisen.weibo.sina.ui.widget.photoview.AttacherInterface.OnMatrixChangedListener; import org.aisen.weibo.sina.ui.widget.photoview.AttacherInterface.OnPhotoTapListener; import org.aisen.weibo.sina.ui.widget.photoview.AttacherInterface.OnViewTapListener; import java.lang.ref.WeakReference; public class PhotoViewAttacher implements IPhotoView, ViewTreeObserver.OnGlobalLayoutListener { static final String LOG_TAG = "PhotoViewAttacher"; // let debug flag be dynamic, but still Proguard can be used to remove from // release builds static final boolean DEBUG = true; static final int EDGE_NONE = -1; static final int EDGE_LEFT = 0; static final int EDGE_RIGHT = 1; static final int EDGE_BOTH = 2; 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; private float mMinScale = DEFAULT_MIN_SCALE; private float mMidScale = DEFAULT_MID_SCALE; private float mMaxScale = DEFAULT_MAX_SCALE; private boolean mAllowParentInterceptOnEdge = true; private static void checkZoomLevels(float minZoom, float midZoom, float maxZoom) { if (minZoom >= midZoom) { throw new IllegalArgumentException("MinZoom should be less than MidZoom"); } else if (midZoom >= maxZoom) { throw new IllegalArgumentException("MidZoom should 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) { if (null != imageView) { if (imageView instanceof PhotoView) { /** * PhotoView sets it's own ScaleType to Matrix, then diverts all calls setScaleType * to this.setScaleType. Basically we don't need to do anything here */ } else { imageView.setScaleType(ScaleType.MATRIX); } } } private WeakReference<ImageView> mImageView; private ViewTreeObserver mViewTreeObserver; // 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 int mIvTop, mIvRight, mIvBottom, mIvLeft; private FlingRunnable mCurrentFlingRunnable; private int mScrollEdge = EDGE_BOTH; private boolean mZoomEnabled; private ScaleType mScaleType = ScaleType.CENTER_CROP; /**/ public float mDrawableWidth, mImageViewWidth, mOffsetX = 0; /**/ public PhotoViewAttacher(ImageView imageView) { mImageView = new WeakReference<ImageView>(imageView); mViewTreeObserver = imageView.getViewTreeObserver(); mViewTreeObserver.addOnGlobalLayoutListener(this); // Make sure we using MATRIX Scale Type setImageViewScaleTypeMatrix(imageView); if (!imageView.isInEditMode()) { setZoomable(true); } } public void setImageViewIfClear(ImageView imageView) { if (mImageView == null || mImageView.get() == null) { mImageView = new WeakReference<ImageView>(imageView); } } public final 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 final void cleanup() { if (null != mImageView) { ImageView iv = mImageView.get(); if (iv != null) { iv.getViewTreeObserver().removeGlobalOnLayoutListener(this); } } mViewTreeObserver = null; // Clear listeners too mMatrixChangeListener = null; // Finally, clear ImageView mImageView = null; } public final RectF getDisplayRect() { checkMatrixBounds(); return getDisplayRect(getDisplayMatrix()); } public final ImageView getImageView() { ImageView imageView = null; if (null != mImageView) { imageView = mImageView.get(); } // If we don't have an ImageView, call cleanup() if (null == imageView) { cleanup(); // throw new // IllegalStateException("ImageView no longer exists. You should not use this PhotoViewAttacher any more."); } return imageView; } public float getMinScale() { return mMinScale; } public float getMidScale() { return mMidScale; } public float getMaxScale() { return mMaxScale; } public final float getScale() { return getValue(mSuppMatrix, Matrix.MSCALE_X); } public final ScaleType getScaleType() { return mScaleType; } public final boolean onDoubleTap(MotionEvent ev) { try { float scale = getScale(); float x = ev.getX(); float y = ev.getY(); if (scale < mMidScale) { zoomTo(mMidScale, x, y); } else if (scale >= mMidScale && scale < mMaxScale) { zoomTo(mMaxScale, x, y); } else { zoomTo(mMinScale, x, y); } } catch (ArrayIndexOutOfBoundsException e) { // Can sometimes happen when getX() and getY() is called } return true; } public final void onMove(float percents) { // LLog.v("onMove percents: " + percents); ImageView imageView = getImageView(); if (null != imageView && hasDrawable(imageView)) { float to = (mDrawableWidth - mImageViewWidth) * (1f - percents) / 2f; mSuppMatrix.postTranslate(to - mOffsetX, 0); // LLog.e("onMove to:" + to + " " + "mOffsetX:" + mOffsetX); mOffsetX = to; checkAndDisplayMatrix(); } } public final void onGlobalLayout() { ImageView imageView = getImageView(); if (null != imageView && 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; } } } public final 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 (hasDrawable(getImageView()) && (getScale() < mMaxScale || scaleFactor < 1f)) { mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); checkAndDisplayMatrix(); } } public void setAllowParentInterceptOnEdge(boolean allow) { mAllowParentInterceptOnEdge = allow; } public void setMinScale(float minScale) { checkZoomLevels(minScale, mMidScale, mMaxScale); mMinScale = minScale; } public void setMidScale(float midScale) { checkZoomLevels(mMinScale, midScale, mMaxScale); mMidScale = midScale; } public void setMaxScale(float maxScale) { checkZoomLevels(mMinScale, mMidScale, maxScale); mMaxScale = maxScale; } public final void setOnLongClickListener(OnLongClickListener listener) { } public final void setOnMatrixChangeListener(OnMatrixChangedListener listener) { } public final void setOnPhotoTapListener(OnPhotoTapListener listener) { } public final void setOnViewTapListener(OnViewTapListener listener) { } public final void setScaleType(ScaleType scaleType) { if (isSupportedScaleType(scaleType) && scaleType != mScaleType) { mScaleType = scaleType; // Finally update update(); } } public final void setZoomable(boolean zoomable) { mZoomEnabled = zoomable; update(); } public final 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 final void zoomTo(float scale, float focalX, float focalY) { ImageView imageView = getImageView(); if (null != imageView) { imageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY)); } } public Matrix getDisplayMatrix() { mDrawMatrix.set(mBaseMatrix); mDrawMatrix.postConcat(mSuppMatrix); return mDrawMatrix; } /** * Helper method that simply checks the Matrix, and then displays the result */ private void checkAndDisplayMatrix() { // LLog.v(getClass().getSimpleName() + " checkAndDisplayMatrix()"); checkMatrixBounds(); setImageViewMatrix(getDisplayMatrix()); } 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 PhotoView)) { if (imageView.getScaleType() != ScaleType.MATRIX) { throw new IllegalStateException( "The ImageView's ScaleType has been changed since attaching a PhotoViewAttacher"); } } } private void checkMatrixBounds() { final ImageView imageView = getImageView(); if (null == imageView) { return; } final RectF rect = getDisplayRect(getDisplayMatrix()); if (null == rect) { return; } final float height = rect.height(), width = rect.width(); float deltaX = 0, deltaY = 0; final int viewHeight = imageView.getHeight(); 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 = imageView.getWidth(); 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); } /** * 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; } /** * 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(getDisplayMatrix()); 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 = imageView.getWidth(); final float viewHeight = imageView.getHeight(); 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); mDrawableWidth = drawableWidth * scale; mImageViewWidth = viewWidth; mOffsetX = (mDrawableWidth - mImageViewWidth) / 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 class AnimatedZoomRunnable implements Runnable { // These are 'postScale' values, means they're compounded each iteration static final float ANIMATION_SCALE_PER_ITERATION_IN = 1.07f; static final float ANIMATION_SCALE_PER_ITERATION_OUT = 0.93f; private final float mFocalX, mFocalY; private final float mTargetZoom; private final float mDeltaScale; public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX, final float focalY) { mTargetZoom = targetZoom; mFocalX = focalX; mFocalY = focalY; if (currentZoom < targetZoom) { mDeltaScale = ANIMATION_SCALE_PER_ITERATION_IN; } else { mDeltaScale = ANIMATION_SCALE_PER_ITERATION_OUT; } } public void run() { ImageView imageView = getImageView(); if (null != imageView) { mSuppMatrix.postScale(mDeltaScale, mDeltaScale, mFocalX, mFocalY); checkAndDisplayMatrix(); final float currentScale = getScale(); if ((mDeltaScale > 1f && currentScale < mTargetZoom) || (mDeltaScale < 1f && mTargetZoom < currentScale)) { // We haven't hit our target scale yet, so post ourselves // again Compat.postOnAnimation(imageView, this); } else { // We've scaled past our target zoom, so calculate the // necessary scale so we're back at target zoom final float delta = mTargetZoom / currentScale; mSuppMatrix.postScale(delta, delta, mFocalX, mFocalY); checkAndDisplayMatrix(); } } } } private class FlingRunnable implements Runnable { private final ScrollerProxy mScroller; private int mCurrentX, mCurrentY; public FlingRunnable(Context context) { mScroller = ScrollerProxy.getScroller(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); } } public void run() { ImageView imageView = null; try { imageView = getImageView(); } catch (Exception e) { e.printStackTrace(); } 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(getDisplayMatrix()); mCurrentX = newX; mCurrentY = newY; // Post On animation Compat.postOnAnimation(imageView, this); } } } }