package com.jparkie.aizoban.views.widgets; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.widget.ImageView; import android.widget.OverScroller; import com.jparkie.aizoban.utils.PreferenceUtils; public class GestureImageView extends ImageView { public static final String VIEW_TYPE_FIT_CENTRE = "Fit Centre"; public static final String VIEW_TYPE_FIT_WIDTH = "Fit Width"; public static final String VIEW_TYPE_FIT_HEIGHT = "Fit Height"; public static final float MIN_SCALE = 1.00f; public static final float MAX_SCALE = 3.00f; private static final float ZOOM_DURATION = 200f; private static final long RUNNABLE_DELAY_MS = 1000 / 60; private String mViewType; private Matrix mBaseMatrix = new Matrix(); private Matrix mSupplementaryMatrix = new Matrix(); private Matrix mDisplayMatrix = new Matrix(); private float[] mMatrixValues = new float[9]; private int mMaximumAcceleratedWidth; private int mMaximumAcceleratedHeight; private float mBitmapWidth; private float mBitmapHeight; private FlingRunnable mFlingRunnable; private ZoomRunnable mZoomRunnable; private ScaleGestureDetector mScaleGestureDetector; private boolean mInitialized; public GestureImageView(Context context) { super(context); initialize(); } public GestureImageView(Context context, AttributeSet attributeSet) { super(context, attributeSet); initialize(); } public GestureImageView(Context context, AttributeSet attributeSet, int definitionStyle) { super(context, attributeSet, definitionStyle); initialize(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (canvas.isHardwareAccelerated()) { mMaximumAcceleratedWidth = canvas.getMaximumBitmapWidth(); mMaximumAcceleratedHeight = canvas.getMaximumBitmapHeight(); } } @Override public boolean onTouchEvent(MotionEvent event) { mScaleGestureDetector.onTouchEvent(event); return true; } @Override public void setImageBitmap(Bitmap bitmap) { super.setImageBitmap(bitmap); if (bitmap != null) { mBitmapWidth = bitmap.getWidth(); mBitmapHeight = bitmap.getHeight(); } } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); if (drawable != null) { mBitmapWidth = drawable.getIntrinsicWidth(); mBitmapHeight = drawable.getIntrinsicHeight(); } } private void initialize() { mScaleGestureDetector = new ScaleGestureDetector(getContext(), new ImageViewScaleGestureListener()); mViewType = PreferenceUtils.getViewType(); } public void initializeView() { if (!mInitialized) { setScaleType(ScaleType.MATRIX); initializeBaseMatrix(); setImageMatrix(getImageViewMatrix()); mInitialized = true; } } private void initializeBaseMatrix() { mBaseMatrix.reset(); float widthScale = Math.min(getWidth() / mBitmapWidth, 2.00f); float heightScale = Math.min(getHeight() / mBitmapHeight, 2.00f); float actualScale = Math.min(widthScale, heightScale); if (mViewType.equals(VIEW_TYPE_FIT_CENTRE)) { actualScale = Math.min(widthScale, heightScale); } else if (mViewType.equals(VIEW_TYPE_FIT_WIDTH)) { actualScale = widthScale; } else if (mViewType.equals(VIEW_TYPE_FIT_HEIGHT)) { actualScale = heightScale; } mBaseMatrix.postScale(actualScale, actualScale); mBaseMatrix.postTranslate((getWidth() - mBitmapWidth * actualScale) / 2.00f, (getHeight() - mBitmapHeight * actualScale) / 2.00f); mInitialized = true; } public boolean isInitialized() { return mInitialized; } private float getTransX(Matrix matrix) { matrix.getValues(mMatrixValues); return mMatrixValues[Matrix.MTRANS_X]; } private float getTransY(Matrix matrix) { matrix.getValues(mMatrixValues); return mMatrixValues[Matrix.MTRANS_Y]; } private float getScaleX(Matrix matrix) { matrix.getValues(mMatrixValues); return mMatrixValues[Matrix.MSCALE_X]; } private float getScaleY(Matrix matrix) { matrix.getValues(mMatrixValues); return mMatrixValues[Matrix.MSCALE_Y]; } public float getScale() { return getScaleX(mSupplementaryMatrix); } private Matrix getImageViewMatrix() { mDisplayMatrix.set(mBaseMatrix); mDisplayMatrix.postConcat(mSupplementaryMatrix); return mDisplayMatrix; } public int getMaximumAcceleratedWidth() { return mMaximumAcceleratedWidth; } public int getMaximumAcceleratedHeight() { return mMaximumAcceleratedHeight; } private void center(boolean centerHorizontal, boolean centerVertical) { Matrix currentImageViewMatrix = getImageViewMatrix(); RectF drawableRectangle = new RectF(0.00f, 0.00f, mBitmapWidth, mBitmapHeight); currentImageViewMatrix.mapRect(drawableRectangle); float height = drawableRectangle.height(); float width = drawableRectangle.width(); float deltaX = 0, deltaY = 0; if (centerHorizontal) { int viewWidth = getWidth(); if (width < viewWidth) { deltaX = (viewWidth - width) / 2 - drawableRectangle.left; } else if (drawableRectangle.left > 0) { deltaX = -drawableRectangle.left; } else if (drawableRectangle.right < viewWidth) { deltaX = viewWidth - drawableRectangle.right; } } if (centerVertical) { int viewHeight = getHeight(); if (height < viewHeight) { deltaY = (viewHeight - height) / 2 - drawableRectangle.top; } else if (drawableRectangle.top > 0) { deltaY = -drawableRectangle.top; } else if (drawableRectangle.bottom < viewHeight) { deltaY = getHeight() - drawableRectangle.bottom; } } mSupplementaryMatrix.postTranslate(deltaX, deltaY); setImageMatrix(getImageViewMatrix()); } public void startFling(float velocityX, float velocityY) { if (mFlingRunnable != null) { mFlingRunnable.cancel(); } mFlingRunnable = new FlingRunnable(velocityX, velocityY); postDelayed(mFlingRunnable, RUNNABLE_DELAY_MS); } public void cancelFling() { if (mFlingRunnable != null) { mFlingRunnable.cancel(); } } private void panBy(float deltaX, float deltaY) { mSupplementaryMatrix.postTranslate(deltaX, deltaY); setImageMatrix(getImageViewMatrix()); } public void postTranslate(float deltaX, float deltaY) { mSupplementaryMatrix.postTranslate(deltaX, deltaY); center(true, true); } public void zoomTo(float scale, float centerX, float centerY) { if (scale > MAX_SCALE) { scale = MAX_SCALE; } if (scale < MIN_SCALE) { scale = MIN_SCALE; } float oldScale = getScaleX(mSupplementaryMatrix); float deltaScale = scale / oldScale; mSupplementaryMatrix.postScale(deltaScale, deltaScale, centerX, centerY); setImageMatrix(getImageViewMatrix()); center(true, true); } private void zoomTo(float scale, float centerX, float centerY, float durationMs) { mZoomRunnable = new ZoomRunnable(scale, centerX, centerY, durationMs); postDelayed(mZoomRunnable, RUNNABLE_DELAY_MS); } public void zoomTo(float scale) { float centerX = getWidth() / 2.00f; float centerY = getHeight() / 2.00f; zoomTo(scale, centerX, centerY, ZOOM_DURATION); } public void zoomToPoint(float scale, float pointX, float pointY) { zoomTo(scale, pointX, pointY, ZOOM_DURATION); } public boolean canScrollParent() { if (mInitialized) { if (getTransX(mDisplayMatrix) == 0) { return true; } else if (mBitmapWidth * getScaleX(mDisplayMatrix) + getTransX(mDisplayMatrix) <= getWidth()) { return true; } return false; } return true; } public Matrix getZoomMatrix() { return mSupplementaryMatrix; } public void setZoomMatrix(Matrix oldSupplementaryMatrix) { float newScaleX = getScaleX(oldSupplementaryMatrix); float newScaleY = getScaleY(oldSupplementaryMatrix); mSupplementaryMatrix.setScale(newScaleX, newScaleY); center(true, true); } private class FlingRunnable implements Runnable { private OverScroller mOverScroller; private int mCurrentX; private int mCurrentY; public FlingRunnable(float inputVelocityX, float inputVelocityY) { mOverScroller = new OverScroller(getContext()); int startX = (int) getTransX(mDisplayMatrix); int startY = (int) getTransY(mDisplayMatrix); int velocityX = (int) inputVelocityX; int velocityY = (int) inputVelocityY; int minX; int maxX; int minY; int maxY; int viewWidth = getMeasuredWidth(); int viewHeight = getMeasuredHeight(); float widthScale = Math.min(getWidth() / mBitmapWidth, 2.00f); float heightScale = Math.min(getHeight() / mBitmapHeight, 2.00f); float actualScale = Math.min(widthScale, heightScale); float redundantSpaceX = viewWidth - (actualScale * mBitmapWidth); float redundantSpaceY = viewHeight - (actualScale * mBitmapHeight); Rect drawableRectangle = new Rect(0, 0, (int) (mBitmapWidth * getScaleX(mDisplayMatrix)), (int) (mBitmapHeight * getScaleY(mDisplayMatrix))); int drawableWidth = drawableRectangle.width(); int drawableHeight = drawableRectangle.height(); if (drawableWidth > viewWidth) { minX = viewWidth - (int) redundantSpaceX - drawableWidth; maxX = 0; } else { minX = startX; maxX = startX; } if (drawableHeight > viewHeight) { minY = viewHeight - (int) redundantSpaceY - drawableHeight; maxY = 0; } else { minY = startY; maxY = startY; } mCurrentX = startX; mCurrentY = startY; mOverScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); } @Override public void run() { if (mOverScroller.isFinished()) { return; } if (mOverScroller.computeScrollOffset()) { int newX = mOverScroller.getCurrX(); int newY = mOverScroller.getCurrY(); int transX = newX - mCurrentX; int transY = newY - mCurrentY; mCurrentX = newX; mCurrentY = newY; postTranslate(transX, transY); postDelayed(this, RUNNABLE_DELAY_MS); } } public void cancel() { if (mOverScroller != null) { mOverScroller.forceFinished(true); } } } private class ZoomRunnable implements Runnable { private float mOldScale; private float mCenterX; private float mCenterY; private float mDurationMs; private float mIncrementPerMs; private long mStartTime; public ZoomRunnable(float scale, float centerX, float centerY, float durationMs) { mOldScale = getScaleX(mSupplementaryMatrix); mCenterX = centerX; mCenterY = centerY; mDurationMs = durationMs; mIncrementPerMs = (scale - mOldScale) / mDurationMs; mStartTime = System.currentTimeMillis(); } @Override public void run() { long now = System.currentTimeMillis(); float currentMs = Math.min(mDurationMs, now - mStartTime); float targetScale = mOldScale + (mIncrementPerMs * currentMs); zoomTo(targetScale, mCenterX, mCenterY); if (currentMs < mDurationMs) { postDelayed(this, RUNNABLE_DELAY_MS); } else { return; } } } private class ImageViewScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector scaleGestureDetector) { if (mInitialized) { float scale = getScale() * scaleGestureDetector.getScaleFactor(); zoomTo(scale, scaleGestureDetector.getFocusX(), scaleGestureDetector.getFocusY()); } return true; } @Override public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { return true; } @Override public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) { // Do Nothing. } } }