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);
}
}
}
}