com.diegocarloslima.byakugallery.TouchImageView.java Source code

Java tutorial

Introduction

Here is the source code for com.diegocarloslima.byakugallery.TouchImageView.java

Source

////////////////////////////////////////////////////////////////////////////////
// ByakuGallery is an open source Android library that allows the visualization
//     of large images with gesture capabilities.
//     This lib is based on AOSP Camera2.
//     Copyright 2013 Diego Carlos Lima
//
//     Licensed under the Apache License, Version 2.0 (the "License");
//     you may not use this file except in compliance with the License.
//     You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
//     Unless required by applicable law or agreed to in writing, software
//     distributed under the License is distributed on an "AS IS" BASIS,
//     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//     See the License for the specific language governing permissions and
//     limitations under the License.
////////////////////////////////////////////////////////////////////////////////

package com.diegocarloslima.byakugallery;

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 String TAG = "TouchImageView";

    private static int ANIMATION_DURATION = 0;

    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 mTranslationX;
    private float mTranslationY;

    private Float mLastFocusX;
    private Float mLastFocusY;

    private final FlingScroller mFlingScroller = new FlingScroller();
    private boolean mIsAnimatingBack;

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

        if (ANIMATION_DURATION == 0)
            ANIMATION_DURATION = context.getResources().getInteger(android.R.integer.config_shortAnimTime);

        final TouchGestureDetector.OnTouchGestureListener listener = new TouchGestureDetector.OnTouchGestureListener() {

            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                return performClick();
            }

            @Override
            public void onLongPress(MotionEvent e) {
                performLongClick();
            }

            @Override
            public boolean onDoubleTap(MotionEvent e) {
                loadMatrixValues();

                // 3 stage scaling
                float targetScale = mCropScale;
                if (mScale == mMaxScale)
                    targetScale = mMinScale;
                else if (mScale >= mCropScale)
                    targetScale = 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(ANIMATION_DURATION);
                startAnimation(animation);

                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                // 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) {
                // 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) {
                mLastFocusX = null;
                mLastFocusY = null;

                return true;
            }

            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                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(mMinScale, 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) {
                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;

                float targetScale = MathUtils.clamp(mScale, mMinScale, mMaxScale);

                clearAnimation();
                final Animation animation = new TouchAnimation(targetScale, targetTranslationX, targetTranslationY);
                animation.setDuration(ANIMATION_DURATION);
                startAnimation(animation);

                mIsAnimatingBack = true;
            }
        };

        mTouchGestureDetector = new TouchGestureDetector(context, listener);

        super.setScaleType(ScaleType.MATRIX);
    }

    private float mMinScale;
    private float mCropScale;
    private float mMaxScale;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int oldMeasuredWidth = getMeasuredWidth();
        final int oldMeasuredHeight = getMeasuredHeight();

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (oldMeasuredWidth != getMeasuredWidth() || oldMeasuredHeight != getMeasuredHeight()) {
            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 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;
    }

    private void resetToInitialState() {
        mMinScale = getMeasuredWidth() / (float) mDrawableIntrinsicWidth;
        mCropScale = getMeasuredHeight() / (float) mDrawableIntrinsicHeight;
        if (mMinScale > mCropScale) {
            float temp = mCropScale;
            mCropScale = mMinScale;
            mMinScale = temp;
        }
        mMaxScale = mCropScale * 4.0f;

        mMatrix.reset();
        mMatrix.postScale(mMinScale, mMinScale);

        final float[] values = new float[9];
        mMatrix.getValues(values);

        final float freeSpaceHorizontal = (getMeasuredWidth() - (mDrawableIntrinsicWidth * mMinScale)) / 2F;
        final float freeSpaceVertical = (getMeasuredHeight() - (mDrawableIntrinsicHeight * mMinScale)) / 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];
    }

    // 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, 1]
    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);
        }
    }
}