/*
* Класс использовался в 2ch Browser <https://github.com/vortexwolf/2ch-Browser/>
*
*/
package nya.miku.wishmaster.lib.gallery;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.ImageView;
import android.widget.OverScroller;
import android.widget.Scroller;
/**
* View для установки {@link GifDrawable} с поддержкой зума (pinch-to-zoom), инерционного скроллинга.
* Поведение при двойных тапах такое же, как у {@link FixedSubsamplingScaleImageView}.
* @author miku-nyan
*
*/
@TargetApi(Build.VERSION_CODES.FROYO)
public class TouchGifView extends ImageView {
Matrix matrix = new Matrix();
static final int NONE = 0;
static final int DRAG = 1;
static final int ZOOM = 2;
int mode = NONE;
boolean isZooming = false;
PointF last = new PointF();
PointF start = new PointF();
float minScale = 1f;
float realScale = 1f;
float maxScale = 3f;
float defaultScale = 2f; //maximum default value
final float cMinScale = 1f;
final float cMaxScale = 3f;
final float cDefaultScale = 2f;
float[] m;
float redundantXSpace, redundantYSpace;
float width, height;
static final int CLICK = 3;
float saveScale = 1f;
float right, bottom, origWidth, origHeight, bmWidth, bmHeight;
ScaleGestureDetector mScaleDetector;
GestureDetector mDoubleTapDetector;
Fling fling;
Context context;
@SuppressLint("ClickableViewAccessibility")
public TouchGifView(Context context) {
super(context);
super.setClickable(true);
this.context = context;
mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
mDoubleTapDetector = new GestureDetector(context, new GestureListener());
matrix.setTranslate(1f, 1f);
m = new float[9];
setImageMatrix(matrix);
setScaleType(ScaleType.MATRIX);
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (fling != null) {
fling.cancelFling();
}
if (!isZooming) {
mScaleDetector.onTouchEvent(event);
mDoubleTapDetector.onTouchEvent(event);
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
PointF curr = new PointF(event.getX(), event.getY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
last.set(event.getX(), event.getY());
start.set(last);
mode = DRAG;
break;
case MotionEvent.ACTION_MOVE:
if (mode == DRAG) {
float deltaX = curr.x - last.x;
float deltaY = curr.y - last.y;
float scaleWidth = Math.round(origWidth * saveScale);
float scaleHeight = Math.round(origHeight * saveScale);
if (scaleWidth < width && scaleHeight < height) {
deltaX = 0;
deltaY = 0;
} else if (scaleWidth < width) {
deltaX = 0;
if (y + deltaY > 0)
deltaY = -y;
else if (y + deltaY < -bottom)
deltaY = -(y + bottom);
} else if (scaleHeight < height) {
deltaY = 0;
if (x + deltaX > 0)
deltaX = -x;
else if (x + deltaX < -right)
deltaX = -(x + right);
} else {
if (x + deltaX > 0)
deltaX = -x;
else if (x + deltaX < -right)
deltaX = -(x + right);
if (y + deltaY > 0)
deltaY = -y;
else if (y + deltaY < -bottom)
deltaY = -(y + bottom);
}
matrix.postTranslate(deltaX, deltaY);
last.set(curr.x, curr.y);
}
break;
case MotionEvent.ACTION_UP:
mode = NONE;
int xDiff = (int) Math.abs(curr.x - start.x);
int yDiff = (int) Math.abs(curr.y - start.y);
if (xDiff < CLICK && yDiff < CLICK)
performClick();
break;
case MotionEvent.ACTION_POINTER_UP:
mode = NONE;
break;
}
setImageMatrix(matrix);
invalidate();
}
return true; // indicate event was handled
}
});
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
bmWidth = drawable.getIntrinsicWidth();
bmHeight = drawable.getIntrinsicHeight();
}
public void setMaxZoom(float x) {
maxScale = x;
}
private void fixTrans() {
matrix.getValues(m);
float transX = m[Matrix.MTRANS_X];
float transY = m[Matrix.MTRANS_Y];
float fixTransX = getFixTrans(transX, width, bmWidth*saveScale/realScale);
float fixTransY = getFixTrans(transY, height, bmHeight*saveScale/realScale);
if (fixTransX != 0 || fixTransY != 0) {
matrix.postTranslate(fixTransX, fixTransY);
}
}
private void fixScaleTrans() {
fixTrans();
float imageWidth = bmWidth*saveScale/realScale;
float imageHeight = bmHeight*saveScale/realScale;
matrix.getValues(m);
if (imageWidth < width) {
m[Matrix.MTRANS_X] = (width - imageWidth) / 2;
}
if (imageHeight < height) {
m[Matrix.MTRANS_Y] = (height - imageHeight) / 2;
}
matrix.setValues(m);
}
private float getFixTrans(float trans, float viewSize, float contentSize) {
float minTrans, maxTrans;
if (contentSize <= viewSize) {
minTrans = 0;
maxTrans = viewSize - contentSize;
} else {
minTrans = viewSize - contentSize;
maxTrans = 0;
}
if (trans < minTrans)
return -trans + minTrans;
if (trans > maxTrans)
return -trans + maxTrans;
return 0;
}
public void scale(float mScaleFactor, float focusX, float focusY) {
float origScale = saveScale;
saveScale *= mScaleFactor;
if (saveScale > maxScale) {
saveScale = maxScale;
mScaleFactor = maxScale / origScale;
} else if (saveScale < minScale) {
saveScale = minScale;
mScaleFactor = minScale / origScale;
}
right = width * saveScale - width - (2 * redundantXSpace * saveScale);
bottom = height * saveScale - height - (2 * redundantYSpace * saveScale);
if (origWidth * saveScale <= width || origHeight * saveScale <= height) {
matrix.postScale(mScaleFactor, mScaleFactor, width / 2, height / 2);
if (mScaleFactor < 1) {
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
if (mScaleFactor < 1) {
if (Math.round(origWidth * saveScale) >= width || Math.round(origHeight * saveScale) >= height) {
if (Math.round(origWidth * saveScale) < width) {
if (y < -bottom)
matrix.postTranslate(0, -(y + bottom));
else if (y > 0)
matrix.postTranslate(0, -y);
} else {
if (x < -right)
matrix.postTranslate(-(x + right), 0);
else if (x > 0)
matrix.postTranslate(-x, 0);
}
}
}
}
} else {
matrix.postScale(mScaleFactor, mScaleFactor, focusX, focusY);
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
if (mScaleFactor < 1) {
if (x < -right)
matrix.postTranslate(-(x + right), 0);
else if (x > 0)
matrix.postTranslate(-x, 0);
if (y < -bottom)
matrix.postTranslate(0, -(y + bottom));
else if (y > 0)
matrix.postTranslate(0, -y);
}
}
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
mode = ZOOM;
return true;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = (float)Math.min(Math.max(.95f, detector.getScaleFactor()), 1.05);
scale(scaleFactor, detector.getFocusX(), detector.getFocusY());
return true;
}
}
private class GestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDoubleTap(MotionEvent e) {
float targetScale;
if (saveScale > realScale * 0.9 && saveScale * 0.9 < realScale) targetScale = maxScale;
else if (saveScale > maxScale * 0.99) targetScale = 1f;
else targetScale = realScale;
DoubleTapZoom doubleTap = new DoubleTapZoom(targetScale, e.getX(), e.getY());
compatibilityPostOnAnimation(doubleTap);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (fling != null) {
fling.cancelFling();
}
fling = new Fling((int) velocityX, (int) velocityY);
compatibilityPostOnAnimation(fling);
return super.onFling(e1, e2, velocityX, velocityY);
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void compatibilityPostOnAnimation(Runnable runnable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
postOnAnimation(runnable);
} else {
postDelayed(runnable, 1000/60);
}
}
private class DoubleTapZoom implements Runnable {
private long startTime;
private static final float ZOOM_TIME = 500;
private float startZoom, targetZoom, bitmapX, bitmapY;
private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
private PointF startTouch, endTouch;
DoubleTapZoom(float targetZoom, float focusX, float focusY) {
isZooming = true;
startTime = System.currentTimeMillis();
this.startZoom = saveScale;
this.targetZoom = targetZoom;
PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false);
this.bitmapX = bitmapPoint.x;
this.bitmapY = bitmapPoint.y;
startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY);
endTouch = new PointF(width / 2, height / 2);
}
@Override
public void run() {
float t = interpolate();
float deltaScale = calculateDeltaScale(t);
scale(deltaScale, bitmapX, bitmapY);
translateImageToCenterTouchPosition(t);
fixScaleTrans();
setImageMatrix(matrix);
if (t < 1f) {
compatibilityPostOnAnimation(this);
} else {
if (saveScale != targetZoom) {
scale (targetZoom/saveScale, bitmapX, bitmapY);
translateImageToCenterTouchPosition(t);
fixScaleTrans();
setImageMatrix(matrix);
}
isZooming = false;
}
}
private float interpolate() {
long currTime = System.currentTimeMillis();
float elapsed = (currTime - startTime) / ZOOM_TIME;
elapsed = Math.min(1f, elapsed);
return interpolator.getInterpolation(elapsed);
}
private float calculateDeltaScale(float t) {
float zoom = startZoom + t * (targetZoom - startZoom);
return zoom / saveScale;
}
private void translateImageToCenterTouchPosition(float t) {
float targetX = startTouch.x + t * (endTouch.x - startTouch.x);
float targetY = startTouch.y + t * (endTouch.y - startTouch.y);
PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY);
matrix.postTranslate(targetX - curr.x, targetY - curr.y);
}
private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) {
matrix.getValues(m);
float origW = getDrawable().getIntrinsicWidth();
float origH = getDrawable().getIntrinsicHeight();
float transX = m[Matrix.MTRANS_X];
float transY = m[Matrix.MTRANS_Y];
float imageWidth = bmWidth*saveScale/realScale;
float imageHeight = bmHeight*saveScale/realScale;
float finalX = ((x - transX) * origW) / imageWidth;
float finalY = ((y - transY) * origH) / imageHeight;
if (clipToBitmap) {
finalX = Math.min(Math.max(finalX, 0), origW);
finalY = Math.min(Math.max(finalY, 0), origH);
}
return new PointF(finalX , finalY);
}
private PointF transformCoordBitmapToTouch(float bx, float by) {
matrix.getValues(m);
float origW = getDrawable().getIntrinsicWidth();
float origH = getDrawable().getIntrinsicHeight();
float px = bx / origW;
float py = by / origH;
float imageWidth = bmWidth*saveScale/realScale;
float imageHeight = bmHeight*saveScale/realScale;
float finalX = m[Matrix.MTRANS_X] + imageWidth * px;
float finalY = m[Matrix.MTRANS_Y] + imageHeight * py;
return new PointF(finalX , finalY);
}
}
private interface IScroller {
public void init(Context context);
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY);
public void forceFinished(boolean finished);
public boolean isFinished();
public boolean computeScrollOffset();
public int getCurrX();
public int getCurrY();
}
private static class OldScroller implements IScroller {
private Scroller scroller;
public void init(Context context) {
scroller = new Scroller(context);
}
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
}
public void forceFinished(boolean finished) {
scroller.forceFinished(finished);
}
public boolean isFinished() {
return scroller.isFinished();
}
public boolean computeScrollOffset() {
return scroller.computeScrollOffset();
}
public int getCurrX() {
return scroller.getCurrX();
}
public int getCurrY() {
return scroller.getCurrY();
}
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
private static class NewScroller implements TouchGifView.IScroller {
private OverScroller overScroller;
public void init(Context context) {
overScroller = new OverScroller(context);
}
public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) {
overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
}
public void forceFinished(boolean finished) {
overScroller.forceFinished(finished);
}
public boolean isFinished() {
return overScroller.isFinished();
}
public boolean computeScrollOffset() {
overScroller.computeScrollOffset();
return overScroller.computeScrollOffset();
}
public int getCurrX() {
return overScroller.getCurrX();
}
public int getCurrY() {
return overScroller.getCurrY();
}
}
private class Fling implements Runnable {
IScroller scroller;
int currX, currY;
Fling(int velocityX, int velocityY) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
scroller = new OldScroller();
} else {
scroller = new NewScroller();
}
scroller.init(context);
matrix.getValues(m);
int startX = (int) m[Matrix.MTRANS_X];
int startY = (int) m[Matrix.MTRANS_Y];
int minX, maxX, minY, maxY;
float imageWidth = bmWidth*saveScale/realScale;
float imageHeight = bmHeight*saveScale/realScale;
if (imageWidth > width) {
minX = (int) (width - imageWidth);
maxX = 0;
} else {
minX = maxX = startX;
}
if (imageHeight > height) {
minY = (int) (height - imageHeight);
maxY = 0;
} else {
minY = maxY = startY;
}
scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX, maxX, minY, maxY);
currX = startX;
currY = startY;
}
public void cancelFling() {
if (scroller != null) {
scroller.forceFinished(true);
}
}
@Override
public void run() {
if (scroller.isFinished()) {
scroller = null;
return;
}
if (scroller.computeScrollOffset()) {
int newX = scroller.getCurrX();
int newY = scroller.getCurrY();
int transX = newX - currX;
int transY = newY - currY;
currX = newX;
currY = newY;
matrix.postTranslate(transX, transY);
fixTrans();
setImageMatrix(matrix);
compatibilityPostOnAnimation(this);
}
}
}
@Override
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
//default scale.
float scaleX = (float)width / (float)bmWidth;
float scaleY = (float)height / (float)bmHeight;
float scale = Math.min(scaleX, scaleY); //to fit the screen
defaultScale = Math.min(cDefaultScale, scale);
minScale = Math.min(cMinScale, scale)/defaultScale;
maxScale = cMaxScale/defaultScale;
realScale = 1f/defaultScale;
matrix.setScale(defaultScale, defaultScale);
setImageMatrix(matrix);
saveScale = 1f;
//Center the image
redundantYSpace = (float)height - (defaultScale * (float)bmHeight) ;
redundantXSpace = (float)width - (defaultScale * (float)bmWidth);
redundantYSpace /= (float)2;
redundantXSpace /= (float)2;
matrix.postTranslate(redundantXSpace, redundantYSpace);
origWidth = width - 2 * redundantXSpace;
origHeight = height - 2 * redundantYSpace;
right = width * saveScale - width - (2 * redundantXSpace * saveScale);
bottom = height * saveScale - height - (2 * redundantYSpace * saveScale);
setImageMatrix(matrix);
}
@Override
public boolean canScrollHorizontally(int direction) {
matrix.getValues(m);
float x = m[Matrix.MTRANS_X];
float imageWidth = bmWidth*saveScale/realScale;
if (imageWidth < width) {
return false;
} else if (x >= -1 && direction < 0) {
return false;
} else if (Math.abs(x) + width + 1 >= imageWidth && direction > 0) {
return false;
}
return true;
}
@Override
public boolean canScrollVertically(int direction) {
matrix.getValues(m);
float y = m[Matrix.MTRANS_Y];
float imageHeight = bmHeight*saveScale/realScale;
if (imageHeight < height) {
return false;
} else if (y >= -1 && direction < 0) {
return false;
} else if (Math.abs(y) + height + 1 >= imageHeight && direction > 0) {
return false;
}
return true;
}
public boolean canScrollHorizontallyOldAPI(int direction) {
return canScrollHorizontally(direction);
}
public boolean canScrollVerticallyOldAPI(int direction) {
return canScrollVertically(direction);
}
}