com.huyn.demogroup.freechild.FixedViewAttacher.java Source code

Java tutorial

Introduction

Here is the source code for com.huyn.demogroup.freechild.FixedViewAttacher.java

Source

/*******************************************************************************
 * Copyright 2011, 2012 Chris Banes.
 * <p>
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.huyn.demogroup.freechild;

import android.content.Context;
import android.graphics.RectF;
import android.support.v4.view.MotionEventCompat;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import android.widget.OverScroller;

import com.huyn.demogroup.leaveblank.Compat;
import com.huyn.demogroup.leaveblank.CustomGestureDetector;
import com.huyn.demogroup.leaveblank.OnAnimEndListener;
import com.huyn.demogroup.leaveblank.OnGestureListener;
import com.huyn.demogroup.leaveblank.OnScaleChangedListener;
import com.huyn.demogroup.leaveblank.OnSingleFlingListener;
import com.huyn.demogroup.leaveblank.PhotoView;
import com.huyn.demogroup.leaveblank.Util;

/**
 * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc.
 * It is made public in case you need to subclass something other than {@link ImageView} and still
 * gain the functionality that {@link PhotoView} offers
 */
public class FixedViewAttacher implements View.OnTouchListener, OnGestureListener, View.OnLayoutChangeListener {

    private static float DEFAULT_MAX_SCALE = 3.0f;
    private static float DEFAULT_MID_SCALE = 1.75f;
    private static float DEFAULT_MIN_SCALE = 0.1f;
    private static int DEFAULT_ZOOM_DURATION = 200;

    private static final int EDGE_NONE = -1;
    private static final int EDGE_LEFT = 0;
    private static final int EDGE_RIGHT = 1;
    private static final int EDGE_BOTH = 2;
    private static int SINGLE_TOUCH = 1;

    private Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
    private int mZoomDuration = DEFAULT_ZOOM_DURATION;
    private float mMinScale = DEFAULT_MIN_SCALE;
    private float mMidScale = DEFAULT_MID_SCALE;
    private float mMaxScale = DEFAULT_MAX_SCALE;

    private boolean mAllowParentInterceptOnEdge = true;
    private boolean mBlockParentIntercept = false;

    private ImageView mImageView;

    // Gesture Detectors
    private GestureDetector mGestureDetector;
    private CustomGestureDetector mScaleDragDetector;

    // Listeners
    private OnScaleChangedListener mScaleChangeListener;
    private OnSingleFlingListener mSingleFlingListener;

    private FlingRunnable mCurrentFlingRunnable;
    private int mScrollEdge = EDGE_BOTH;

    private boolean mStable = false;

    private boolean mZoomEnabled = true;

    public FixedViewAttacher(ImageView imageView) {
        mImageView = imageView;

        if (imageView.isInEditMode()) {
            return;
        }

        View parent = (View) mImageView.getParent();
        parent.setOnTouchListener(this);
        parent.addOnLayoutChangeListener(this);

        // Create Gesture Detectors...
        mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), this);

        mGestureDetector = new GestureDetector(imageView.getContext(),
                new GestureDetector.SimpleOnGestureListener() {

                    @Override
                    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                        if (mSingleFlingListener != null) {
                            if (getScale() > DEFAULT_MIN_SCALE) {
                                return false;
                            }

                            if (MotionEventCompat.getPointerCount(e1) > SINGLE_TOUCH
                                    || MotionEventCompat.getPointerCount(e2) > SINGLE_TOUCH) {
                                return false;
                            }

                            return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY);
                        }
                        return false;
                    }
                });
    }

    public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) {
        this.mScaleChangeListener = onScaleChangeListener;
    }

    public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {
        this.mSingleFlingListener = onSingleFlingListener;
    }

    @Deprecated
    public boolean isZoomEnabled() {
        return mZoomEnabled;
    }

    public RectF getDisplayRect() {
        checkMatrixBounds();
        return getVisibleRect();
    }

    public float getMinimumScale() {
        return mMinScale;
    }

    public float getMediumScale() {
        return mMidScale;
    }

    public float getMaximumScale() {
        return mMaxScale;
    }

    public float getScale() {
        return mImageView.getScaleX();
    }

    @Override
    public void onDrag(float dx, float dy) {
        if (mScaleDragDetector.isScaling()) {
            return; // Do not drag if we are already scaling
        }

        System.out.println("++++ondrag:" + dx + "/" + dy);
        postTranslate(dx, dy);
        checkAndDisplayMatrix();

        /*
         * Here we decide whether to let the ImageView's parent to start taking
         * over the touch event.
         *
         * First we check whether this function is enabled. We never want the
         * parent to take over if we're scaling. We then check the edge we're
         * on, and the direction of the scroll (i.e. if we're pulling against
         * the edge, aka 'overscrolling', let the parent take over).
         */
        ViewParent parent = mImageView.getParent();
        if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) {
            if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f)
                    || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
            }
        } else {
            if (parent != null) {
                parent.requestDisallowInterceptTouchEvent(true);
            }
        }
    }

    @Override
    public void onFling(float startX, float startY, float velocityX, float velocityY) {
        mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext());
        mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), getImageViewHeight(mImageView), (int) velocityX,
                (int) velocityY);
        mImageView.post(mCurrentFlingRunnable);
    }

    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop,
            int oldRight, int oldBottom) {
        // Update our base matrix, as the bounds have changed
        System.out.println("===========onLayoutChange.updateBaseMatrix");
        updateBaseMatrix();
    }

    @Override
    public void onScale(float scaleFactor, float focusX, float focusY) {
        System.out.println("+++++++scaleFactor:" + scaleFactor + "/" + getScale());
        if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) {
            if (mScaleChangeListener != null) {
                mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY);
            }
            doScale(scaleFactor);
            checkAndDisplayMatrix();
        }
    }

    /**
     * TODO
     * @param scaleFactor
     */
    private void doScale(float scaleFactor) {
        float scale = scaleFactor * getScale();
        mImageView.setScaleX(scale);
        mImageView.setScaleY(scale);
        System.out.println("++++current scale:" + scale);
    }

    /**
     * TODO
     * @param dx
     * @param dy
     */
    private void postTranslate(float dx, float dy) {
        float tx = mImageView.getTranslationX();
        float ty = mImageView.getTranslationY();
        tx += dx;
        ty += dy;
        mImageView.setTranslationX(tx);
        mImageView.setTranslationY(ty);
        System.out.println("+++++current tx:" + tx + "/ty:" + ty);
    }

    @Override
    public boolean onTouch(View v, MotionEvent ev) {
        boolean handled = false;

        if (mZoomEnabled) {
            switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                ViewParent parent = v.getParent();
                // First, disable the Parent from intercepting the touch
                // event
                if (parent != null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }

                // If we're flinging, and the user presses down, cancel
                // fling
                cancelFling();
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // If the user has zoomed less than min scale, zoom back
                // to min scale
                if (getScale() < mMinScale) {
                    RectF rect = getDisplayRect();
                    if (rect != null) {
                        v.post(new AnimatedZoomRunnable(getScale(), mMinScale, rect.centerX(), rect.centerY(),
                                null));
                        handled = true;
                    }
                }
                break;
            }

            // Try the Scale/Drag detector
            if (mScaleDragDetector != null) {
                boolean wasScaling = mScaleDragDetector.isScaling();
                boolean wasDragging = mScaleDragDetector.isDragging();

                handled = mScaleDragDetector.onTouchEvent(ev);

                boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
                boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();

                mBlockParentIntercept = didntScale && didntDrag;
            }

            // Check to see if the user double tapped
            if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {
                handled = true;
            }

        }

        return handled;
    }

    public void setAllowParentInterceptOnEdge(boolean allow) {
        mAllowParentInterceptOnEdge = allow;
    }

    public void setMinimumScale(float minimumScale) {
        Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale);
        mMinScale = minimumScale;
    }

    public void setMediumScale(float mediumScale) {
        Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale);
        mMidScale = mediumScale;
    }

    public void setMaximumScale(float maximumScale) {
        Util.checkZoomLevels(mMinScale, mMidScale, maximumScale);
        mMaxScale = maximumScale;
    }

    public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
        Util.checkZoomLevels(minimumScale, mediumScale, maximumScale);
        mMinScale = minimumScale;
        mMidScale = mediumScale;
        mMaxScale = maximumScale;
    }

    public void setScale(float scale) {
        setScale(scale, false);
    }

    public void setScale(float scale, boolean animate) {
        setScale(scale, (mImageView.getRight()) / 2, (mImageView.getBottom()) / 2, animate, null);
    }

    public void setScale(float scale, boolean animate, OnAnimEndListener listener) {
        setScale(scale, (mImageView.getRight()) / 2, (mImageView.getBottom()) / 2, animate, listener);
    }

    public void setScale(float scale, float focalX, float focalY, boolean animate) {
        setScale(scale, focalX, focalY, animate, null);
    }

    public void setScale(float scale, float focalX, float focalY, boolean animate, OnAnimEndListener listener) {
        // Check to see if the scale is within bounds
        if (scale < mMinScale || scale > mMaxScale) {
            throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale");
        }

        if (animate) {
            mImageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY, listener));
        } else {
            doScale(scale);
            checkAndDisplayMatrix();
        }
    }

    public void setStable(boolean stable) {
        mStable = stable;
        //update();
    }

    public boolean getStable() {
        return mStable;
    }

    /**
     * Set the zoom interpolator
     *
     * @param interpolator the zoom interpolator
     */
    public void setZoomInterpolator(Interpolator interpolator) {
        mInterpolator = interpolator;
    }

    public boolean isZoomable() {
        return mZoomEnabled;
    }

    public void setZoomable(boolean zoomable) {
        mZoomEnabled = zoomable;
        update();
    }

    public void update() {
        System.out.println("===========update.updateBaseMatrix");
        if (mZoomEnabled) {
            // Update the base matrix using the current drawable
            updateBaseMatrix();
        } else {
            // Reset the Matrix...
            resetMatrix();
        }
    }

    public void setZoomTransitionDuration(int milliseconds) {
        this.mZoomDuration = milliseconds;
    }

    /**
     * Resets the Matrix back to FIT_CENTER, and then displays its contents
     */
    private void resetMatrix() {
        checkMatrixBounds();
    }

    /**
     * Helper method that simply checks the Matrix, and then displays the result
     */
    private void checkAndDisplayMatrix() {
        if (checkMatrixBounds()) {
        }
    }

    /**
     * Helper method that maps the supplied Matrix to the current Drawable
     *
     * @return RectF - Displayed Rectangle
     */
    private RectF getVisibleRect() {
        /*View parent = (View) mImageView.getParent();
        RectF mDisplayRect = new RectF();
        Rect mResult = new Rect();
        //parent.getGlobalVisibleRect(mResult);
        parent.getLocalVisibleRect(mResult);
        mDisplayRect.set(mResult);*/
        RectF mDisplayRect = new RectF();
        float tx = mImageView.getTranslationX();
        float ty = mImageView.getTranslationY();
        mDisplayRect.set(tx, ty, tx + mImageView.getWidth(), ty + mImageView.getHeight());
        return mDisplayRect;
    }

    /**
     * Calculate Matrix for FIT_CENTER
     */
    private void updateBaseMatrix() {
        final float viewWidth = getImageViewWidth(mImageView);
        final float viewHeight = getImageViewHeight(mImageView);
        View parent = (View) mImageView.getParent();
        final int parentWidth = parent.getWidth();
        final int parentHeight = parent.getHeight();

        final float widthScale = parentWidth / viewWidth;
        final float heightScale = parentHeight / viewHeight;

        System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++");
        System.out.println("++++++widthScale:" + widthScale + "/heightScale:" + heightScale);
        System.out.println("++++++viewWidth:" + viewWidth + "/viewHeight:" + viewHeight);
        System.out.println("++++++parentWidth:" + parentWidth + "/parentHeight:" + parentHeight);

        float scale = Math.max(widthScale, heightScale);
        System.out.println("++++scale:" + scale);
        doScale(scale);
        float tx = (parentWidth - viewWidth * scale) / 2F;
        float ty = (parentHeight - viewHeight * scale) / 2F;
        System.out.println("++++tx:" + tx + "/ty:" + ty);

        //        postTranslate(tx, ty);

        mImageView.setTranslationX(tx);
        mImageView.setTranslationY(ty);

        resetMatrix();
    }

    private boolean checkMatrixBounds() {
        final RectF rect = getVisibleRect();
        if (rect == null) {
            return false;
        }

        RectF src = new RectF();
        src.set(rect);
        final float h = rect.height(), w = rect.width();
        float deltaX = 0, deltaY = 0;
        View parent = (View) mImageView.getParent();

        final int viewHeight = parent.getHeight();//getImageViewHeight(mImageView);
        final int viewWidth = parent.getWidth();//getImageViewWidth(mImageView);
        System.out.println("===============================================width:" + viewWidth + "/" + viewHeight);
        System.out
                .println("+++++++++rect.left:" + rect.left + "/" + rect.top + "/" + rect.right + "/" + rect.bottom);

        float scale = getScale();
        float centerX = rect.centerX();
        float centerY = rect.centerY();
        float targetW = w * scale;
        float targetH = h * scale;
        RectF targetRect = new RectF(centerX - targetW / 2, centerY - targetH / 2, centerX + targetW / 2,
                centerY + targetH / 2);

        rect.set(targetRect);
        final float height = rect.height(), width = rect.width();

        if (height <= viewHeight) {
            deltaY = (viewHeight - height) / 2 - rect.top;
        } else if (rect.top > 0) {
            deltaY = -rect.top;
        } else if (rect.bottom < viewHeight) {
            deltaY = viewHeight - rect.bottom;
        }

        if (width <= viewWidth) {
            deltaX = (viewWidth - width) / 2 - rect.left;
            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
        if (!mStable) {
            System.out.println("+++++++++++deltaX:" + deltaX + "/deltaY:" + deltaY + "---" + getScale());
            postTranslate(deltaX, deltaY);
        }
        return true;
    }

    private int getImageViewWidth(ImageView imageView) {
        return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight();
    }

    private int getImageViewHeight(ImageView imageView) {
        return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom();
    }

    private void cancelFling() {
        if (mCurrentFlingRunnable != null) {
            mCurrentFlingRunnable.cancelFling();
            mCurrentFlingRunnable = null;
        }
    }

    private class AnimatedZoomRunnable implements Runnable {

        private final float mFocalX, mFocalY;
        private final long mStartTime;
        private final float mZoomStart, mZoomEnd;
        private final OnAnimEndListener mListener;

        public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, final float focalX,
                final float focalY, final OnAnimEndListener listener) {
            mFocalX = focalX;
            mFocalY = focalY;
            mStartTime = System.currentTimeMillis();
            mZoomStart = currentZoom;
            mZoomEnd = targetZoom;
            mListener = listener;
        }

        @Override
        public void run() {

            float t = interpolate();
            float scale = mZoomStart + t * (mZoomEnd - mZoomStart);
            float deltaScale = scale / getScale();

            onScale(deltaScale, mFocalX, mFocalY);

            System.out.println("+++++++scale:" + scale + "/end:" + mZoomEnd + "---t:" + t);

            // We haven't hit our target scale yet, so post ourselves again
            if (t < 1f) {
                Compat.postOnAnimation(mImageView, this);
            } else if (mListener != null) {
                mImageView.post(new Runnable() {
                    @Override
                    public void run() {
                        mListener.onAnimEnd();
                    }
                });
            }
        }

        private float interpolate() {
            float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration;
            t = Math.min(1f, t);
            t = mInterpolator.getInterpolation(t);
            return t;
        }
    }

    private class FlingRunnable implements Runnable {

        private final OverScroller mScroller;
        private int mCurrentX, mCurrentY;

        public FlingRunnable(Context context) {
            mScroller = new OverScroller(context);
        }

        public void cancelFling() {
            mScroller.forceFinished(true);
        }

        public void fling(int viewWidth, int viewHeight, int velocityX, int velocityY) {
            final RectF rect = getDisplayRect();
            if (rect == null) {
                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 we actually can move, fling the scroller
            if (startX != maxX || startY != maxY) {
                mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
            }
        }

        @Override
        public void run() {
            if (mScroller.isFinished()) {
                return; // remaining post that should not be handled
            }

            //            if (mScroller.computeScrollOffset()) {
            //
            //                final int newX = mScroller.getCurrX();
            //                final int newY = mScroller.getCurrY();
            //
            //                postTranslate(mCurrentX - newX, mCurrentY - newY);
            //
            //                mCurrentX = newX;
            //                mCurrentY = newY;
            //
            //                // Post On animation
            //                Compat.postOnAnimation(mImageView, this);
            //            }
        }
    }
}