package com.diegocarloslima.byakugallery.lib;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.Transformation;
import android.widget.ImageView;
public class TouchImageView extends ImageView {
private static final int DOUBLE_TAP_ANIMATION_DURATION = 300;
private static final int SCALE_END_ANIMATION_DURATION = 200;
private Drawable mDrawable;
private int mDrawableIntrinsicWidth;
private int mDrawableIntrinsicHeight;
private final TouchGestureDetector mTouchGestureDetector;
private final Matrix mMatrix = new Matrix();
private final float[] mMatrixValues = new float[9];
private float mScale;
private float mMaxScale;
private float mTranslationX;
private float mTranslationY;
private Float mLastFocusX;
private Float mLastFocusY;
private final FlingScroller mFlingScroller = new FlingScroller();
private boolean mIsAnimatingBack;
private boolean mIsScaling;
public TouchImageView(Context context) {
this(context, null);
}
public TouchImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TouchImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TouchGestureDetector.OnTouchGestureListener listener = new TouchGestureDetector.OnTouchGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
return performClick();
}
@Override
public void onLongPress(MotionEvent e) {
if (mIsScaling == false) {
performLongClick();
}
}
@Override
public boolean onDoubleTap(MotionEvent e) {
if (mDrawable == null) {
return false;
}
loadMatrixValues();
final float minScale = getMinScale();
// If we have already zoomed in, we should return to our initial scale value (minScale). Otherwise, scale to full size
final float targetScale = mScale > minScale ? minScale : mMaxScale;
// First, we try to keep the focused point in the same position when the animation ends
final float desiredTranslationX = e.getX() - (e.getX() - mTranslationX) * (targetScale / mScale);
final float desiredTranslationY = e.getY() - (e.getY() - mTranslationY) * (targetScale / mScale);
// Here, we apply a correction to avoid unwanted blank spaces
final float targetTranslationX = desiredTranslationX + computeTranslation(getMeasuredWidth(), mDrawableIntrinsicWidth * targetScale, desiredTranslationX, 0);
final float targetTranslationY = desiredTranslationY + computeTranslation(getMeasuredHeight(), mDrawableIntrinsicHeight * targetScale, desiredTranslationY, 0);
clearAnimation();
final Animation animation = new TouchAnimation(targetScale, targetTranslationX, targetTranslationY);
animation.setDuration(DOUBLE_TAP_ANIMATION_DURATION);
startAnimation(animation);
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (mDrawable == null) {
return false;
}
// Sometimes, this method is called just after an onScaleEnd event. In this case, we want to wait until we animate back our image
if(mIsAnimatingBack) {
return false;
}
loadMatrixValues();
final float currentDrawableWidth = mDrawableIntrinsicWidth * mScale;
final float currentDrawableHeight = mDrawableIntrinsicHeight * mScale;
final float dx = computeTranslation(getMeasuredWidth(), currentDrawableWidth, mTranslationX, -distanceX);
final float dy = computeTranslation(getMeasuredHeight(), currentDrawableHeight, mTranslationY, -distanceY);
mMatrix.postTranslate(dx, dy);
clearAnimation();
ViewCompat.postInvalidateOnAnimation(TouchImageView.this);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (mDrawable == null) {
return false;
}
// Sometimes, this method is called just after an onScaleEnd event. In this case, we want to wait until we animate back our image
if(mIsAnimatingBack) {
return false;
}
loadMatrixValues();
final float horizontalSideFreeSpace = (getMeasuredWidth() - mDrawableIntrinsicWidth * mScale) / 2F;
final float minTranslationX = horizontalSideFreeSpace > 0 ? horizontalSideFreeSpace : getMeasuredWidth() - mDrawableIntrinsicWidth * mScale;
final float maxTranslationX = horizontalSideFreeSpace > 0 ? horizontalSideFreeSpace : 0;
final float verticalSideFreeSpace = (getMeasuredHeight() - mDrawableIntrinsicHeight * mScale) / 2F;
final float minTranslationY = verticalSideFreeSpace > 0 ? verticalSideFreeSpace : getMeasuredHeight() - mDrawableIntrinsicHeight * mScale;
final float maxTranslationY = verticalSideFreeSpace > 0 ? verticalSideFreeSpace : 0;
// Using FlingScroller here. The results were better than the Scroller class
// https://android.googlesource.com/platform/packages/apps/Gallery2/+/master/src/com/android/gallery3d/ui/FlingScroller.java
mFlingScroller.fling(Math.round(mTranslationX), Math.round(mTranslationY), Math.round(velocityX), Math.round(velocityY), Math.round(minTranslationX), Math.round(maxTranslationX), Math.round(minTranslationY), Math.round(maxTranslationY));
clearAnimation();
final Animation animation = new FlingAnimation();
animation.setDuration(mFlingScroller.getDuration());
animation.setInterpolator(new LinearInterpolator());
startAnimation(animation);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
mIsScaling = true;
mLastFocusX = null;
mLastFocusY = null;
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mDrawable == null) {
return false;
}
loadMatrixValues();
float currentDrawableWidth = mDrawableIntrinsicWidth * mScale;
float currentDrawableHeight = mDrawableIntrinsicHeight * mScale;
final float focusX = computeFocus(getMeasuredWidth(), currentDrawableWidth, mTranslationX, detector.getFocusX());
final float focusY = computeFocus(getMeasuredHeight(), currentDrawableHeight, mTranslationY, detector.getFocusY());
// Here, we provide the ability to scroll while scaling
if(mLastFocusX != null && mLastFocusY != null) {
final float dx = computeScaleTranslation(getMeasuredWidth(), currentDrawableWidth, mTranslationX, focusX - mLastFocusX);
final float dy = computeScaleTranslation(getMeasuredHeight(), currentDrawableHeight, mTranslationY, focusY - mLastFocusY);
if(dx != 0 || dy != 0) {
mMatrix.postTranslate(dx, dy);
}
}
final float scale = computeScale(getMinScale(), mMaxScale, mScale, detector.getScaleFactor());
mMatrix.postScale(scale, scale, focusX, focusY);
mLastFocusX = focusX;
mLastFocusY = focusY;
clearAnimation();
ViewCompat.postInvalidateOnAnimation(TouchImageView.this);
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
mIsScaling = false;
if (mDrawable == null) {
return;
}
loadMatrixValues();
final float currentDrawableWidth = mDrawableIntrinsicWidth * mScale;
final float currentDrawableHeight = mDrawableIntrinsicHeight * mScale;
final float dx = computeTranslation(getMeasuredWidth(), currentDrawableWidth, mTranslationX, 0);
final float dy = computeTranslation(getMeasuredHeight(), currentDrawableHeight, mTranslationY, 0);
if(Math.abs(dx) < 1 && Math.abs(dy) < 1) {
return;
}
final float targetTranslationX = mTranslationX + dx;
final float targetTranslationY = mTranslationY + dy;
clearAnimation();
final Animation animation = new TouchAnimation(mScale, targetTranslationX, targetTranslationY);
animation.setDuration(SCALE_END_ANIMATION_DURATION);
startAnimation(animation);
mIsAnimatingBack = true;
}
};
mTouchGestureDetector = new TouchGestureDetector(context, listener);
super.setScaleType(ScaleType.MATRIX);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int oldMeasuredWidth = getMeasuredWidth();
final int oldMeasuredHeight = getMeasuredHeight();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
// set max scale to fit measured size
if(mMaxScale == 0f && mDrawableIntrinsicWidth > 0 && mDrawableIntrinsicHeight>0){
float maxScale = Math.max(getMeasuredWidth() / (float)mDrawableIntrinsicWidth, getMeasuredHeight() / (float)mDrawableIntrinsicHeight);
setMaxScale(maxScale);
}
if(oldMeasuredWidth != measuredWidth || oldMeasuredHeight != measuredHeight) {
resetToInitialState();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.setImageMatrix(mMatrix);
super.onDraw(canvas);
}
@Override
public void setImageMatrix(Matrix matrix) {
if(matrix == null) {
matrix = new Matrix();
}
if(!mMatrix.equals(matrix)) {
mMatrix.set(matrix);
invalidate();
}
}
@Override
public Matrix getImageMatrix() {
return mMatrix;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mTouchGestureDetector.onTouchEvent(event);
return true;
}
@Override
public void clearAnimation() {
super.clearAnimation();
mIsAnimatingBack = false;
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
if(mDrawable != drawable) {
mDrawable = drawable;
if(drawable != null) {
mDrawableIntrinsicWidth = drawable.getIntrinsicWidth();
mDrawableIntrinsicHeight = drawable.getIntrinsicHeight();
resetToInitialState();
} else {
mDrawableIntrinsicWidth = 0;
mDrawableIntrinsicHeight = 0;
}
}
}
@Override
public void setScaleType(ScaleType scaleType) {
if(scaleType != ScaleType.MATRIX) {
throw new IllegalArgumentException("Unsupported scaleType. Only ScaleType.MATRIX is allowed.");
}
super.setScaleType(scaleType);
}
@Override
public boolean canScrollHorizontally(int direction) {
loadMatrixValues();
if(direction > 0) {
return Math.round(mTranslationX) < 0;
} else if(direction < 0) {
final float currentDrawableWidth = mDrawableIntrinsicWidth * mScale;
return Math.round(mTranslationX) > getMeasuredWidth() - Math.round(currentDrawableWidth);
}
return false;
}
public void setMaxScale(float maxScale) {
mMaxScale = maxScale;
}
private void resetToInitialState() {
mMatrix.reset();
final float minScale = getMinScale();
// no measured size
if(minScale == 0f)
return;
mMatrix.postScale(minScale, minScale);
final float freeSpaceHorizontal = (getMeasuredWidth() - (mDrawableIntrinsicWidth * minScale)) / 2F;
final float freeSpaceVertical = (getMeasuredHeight() - (mDrawableIntrinsicHeight * minScale)) / 2F;
mMatrix.postTranslate(freeSpaceHorizontal, freeSpaceVertical);
invalidate();
}
private void loadMatrixValues() {
mMatrix.getValues(mMatrixValues);
mScale = mMatrixValues[Matrix.MSCALE_X];
mTranslationX = mMatrixValues[Matrix.MTRANS_X];
mTranslationY = mMatrixValues[Matrix.MTRANS_Y];
}
private float getMinScale() {
if(getMeasuredWidth() == 0 || getMeasuredHeight() == 0 || mDrawableIntrinsicWidth == 0 || mDrawableIntrinsicHeight == 0) return 0f;
float minScale = Math.min(getMeasuredWidth() / (float) mDrawableIntrinsicWidth, getMeasuredHeight() / (float) mDrawableIntrinsicHeight);
if(minScale > mMaxScale && mMaxScale != 0f) {
minScale = mMaxScale;
}
return minScale;
}
// The translation values must be in [0, viewSize - drawableSize], except if we have free space. In that case we will translate to half of the free space
private static float computeTranslation(float viewSize, float drawableSize, float currentTranslation, float delta) {
final float sideFreeSpace = (viewSize - drawableSize) / 2F;
if(sideFreeSpace > 0) {
return sideFreeSpace - currentTranslation;
} else if(currentTranslation + delta > 0) {
return -currentTranslation;
} else if(currentTranslation + delta < viewSize - drawableSize) {
return viewSize - drawableSize - currentTranslation;
}
return delta;
}
private static float computeScaleTranslation(float viewSize, float drawableSize, float currentTranslation, float delta) {
final float minTranslation = viewSize > drawableSize ? 0 : viewSize - drawableSize;
final float maxTranslation = viewSize > drawableSize ? viewSize - drawableSize : 0;
if(currentTranslation < minTranslation && delta > 0) {
if(currentTranslation + delta > maxTranslation) {
return maxTranslation - currentTranslation;
} else {
return delta;
}
} else if(currentTranslation > maxTranslation && delta < 0) {
if(currentTranslation + delta < minTranslation) {
return minTranslation - currentTranslation;
}
else {
return delta;
}
} else if(currentTranslation > minTranslation && currentTranslation < maxTranslation) {
if(currentTranslation + delta < minTranslation) {
return minTranslation - currentTranslation;
} else if(currentTranslation + delta > maxTranslation) {
return maxTranslation - currentTranslation;
} else {
return delta;
}
}
return 0;
}
// If our focal point is outside the image, we will project it to our image bounds
private static float computeFocus(float viewSize, float drawableSize, float currentTranslation, float focusCoordinate) {
if(currentTranslation > 0 && focusCoordinate < currentTranslation) {
return currentTranslation;
} else if(currentTranslation < viewSize - drawableSize && focusCoordinate > currentTranslation + drawableSize) {
return drawableSize + currentTranslation;
}
return focusCoordinate;
}
// The scale values must be in [minScale, maxScale]
private static float computeScale(float minScale, float maxScale, float currentScale, float delta) {
if(currentScale * delta < minScale) {
return minScale / currentScale;
} else if(currentScale * delta > maxScale) {
return maxScale / currentScale;
}
return delta;
}
private class FlingAnimation extends Animation {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
mFlingScroller.computeScrollOffset(interpolatedTime);
loadMatrixValues();
final float dx = mFlingScroller.getCurrX() - mTranslationX;
final float dy = mFlingScroller.getCurrY() - mTranslationY;
mMatrix.postTranslate(dx, dy);
ViewCompat.postInvalidateOnAnimation(TouchImageView.this);
}
}
private class TouchAnimation extends Animation {
private float initialScale;
private float initialTranslationX;
private float initialTranslationY;
private float targetScale;
private float targetTranslationX;
private float targetTranslationY;
TouchAnimation(float targetScale, float targetTranslationX, float targetTranslationY) {
loadMatrixValues();
this.initialScale = mScale;
this.initialTranslationX = mTranslationX;
this.initialTranslationY = mTranslationY;
this.targetScale = targetScale;
this.targetTranslationX = targetTranslationX;
this.targetTranslationY = targetTranslationY;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
loadMatrixValues();
if(interpolatedTime >= 1) {
mMatrix.getValues(mMatrixValues);
mMatrixValues[Matrix.MSCALE_X] = this.targetScale;
mMatrixValues[Matrix.MSCALE_Y] = this.targetScale;
mMatrixValues[Matrix.MTRANS_X] = this.targetTranslationX;
mMatrixValues[Matrix.MTRANS_Y] = this.targetTranslationY;
mMatrix.setValues(mMatrixValues);
} else {
final float scaleFactor = (this.initialScale + interpolatedTime * (this.targetScale - this.initialScale)) / mScale;
mMatrix.postScale(scaleFactor, scaleFactor);
mMatrix.getValues(mMatrixValues);
final float currentTranslationX = mMatrixValues[Matrix.MTRANS_X];
final float currentTranslationY = mMatrixValues[Matrix.MTRANS_Y];
final float dx = this.initialTranslationX + interpolatedTime * (this.targetTranslationX - this.initialTranslationX) - currentTranslationX;
final float dy = this.initialTranslationY + interpolatedTime * (this.targetTranslationY - this.initialTranslationY) - currentTranslationY;
mMatrix.postTranslate(dx, dy);
}
ViewCompat.postInvalidateOnAnimation(TouchImageView.this);
}
}
}