Android Open Source - GestureViews Gestures Controller






From Project

Back to project page GestureViews.

License

The source code is released under:

Apache License

If you think the Android project GestureViews listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.alexvasilkov.gestures;
/*from ww w . jav a  2 s .c o m*/
import android.content.Context;
import android.graphics.PointF;
import android.os.Handler;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.*;
import android.widget.OverScroller;
import com.alexvasilkov.gestures.detectors.RotationGestureDetector;
import com.alexvasilkov.gestures.detectors.ScaleGestureDetectorFixed;
import com.alexvasilkov.gestures.utils.FloatScroller;
import com.alexvasilkov.gestures.utils.MovementBounds;

/**
 * Main logic to update view state ({@link State}) basing on screen touches.
 * <p/>
 * This class implements {@link android.view.View.OnTouchListener} and provides
 * {@link GesturesController.OnStateChangedListener} to listen for state changes.
 * <p/>
 * Settings can be provided through {@link #getSettings()}. Note, that some settings are required,
 * see {@link Settings}.
 */
public class GesturesController extends GesturesAdapter {

    private static final float ZOOM_GESTURE_MIN_SPAN_DP = 20f;
    private static final float FLING_COEFFICIENT = 0.75f;

    // Control constants converted to pixels
    private final float mZoomGestureMinSpan;
    private final int mTouchSlop, mMinimumVelocity, mMaximumVelocity;

    private final OnStateChangedListener mStateListener;

    private final AnimationTick mAnimationTick;

    // Various gesture detectors
    private final GestureDetector mGestureDetector;
    private final ScaleGestureDetector mScaleDetector;
    private final RotationGestureDetector mRotateDetector;

    private boolean mIsDoubleTapDetected;
    private boolean mIsScrollDetected;
    private boolean mIsFlingDetected;
    private boolean mIsScaleDetected;
    private float mPivotX, mPivotY;

    private final OverScroller mFlingScroller;
    private final FloatScroller mStateScroller;

    private final MovementBounds mFlingBounds = new MovementBounds();
    private final State mPrevState = new State(), mStateStart = new State(), mStateEnd = new State();

    private final Settings mSettings;
    private final State mState = new State();
    private final StateController mStateController;

    private OnGestureListener mGestureListener;

    public GesturesController(Context context, OnStateChangedListener listener) {
        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
        mZoomGestureMinSpan = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, ZOOM_GESTURE_MIN_SPAN_DP, metrics);

        mSettings = new Settings(context);
        mStateController = new StateController(mSettings);

        mStateListener = listener;

        mAnimationTick = new AnimationTick();
        mGestureDetector = new GestureDetector(context, this);
        mGestureDetector.setIsLongpressEnabled(false);
        mScaleDetector = new ScaleGestureDetectorFixed(context, this);
        mRotateDetector = new RotationGestureDetector(context, this);

        mFlingScroller = new OverScroller(context);
        mStateScroller = new FloatScroller(context);

        final ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    }


    /**
     * Sets listener for basic touch events. See {@link com.alexvasilkov.gestures.GesturesController.OnGestureListener}
     */
    public void setOnGesturesListener(OnGestureListener listener) {
        mGestureListener = listener;
    }

    /**
     * Sets whether long press is enabled or not. Long press is disabled by default.
     * <p/>
     * See also {@link com.alexvasilkov.gestures.GesturesController.OnGestureListener#onLongPress(android.view.MotionEvent)}
     */
    public void setLongPressEnabled(boolean enabled) {
        mGestureDetector.setIsLongpressEnabled(enabled);
    }

    /**
     * Returns settings that can be updated.
     * <p/>
     * Note: always call {@link #updateState()} or {@link #resetState()} after settings was changed
     * to correctly apply state restrictions.
     */
    public Settings getSettings() {
        return mSettings;
    }

    /**
     * Returns current state. In most cases you should not modify state directly,
     * use one of the methods provided in {@link com.alexvasilkov.gestures.StateController} instead.
     * <p/>
     * If current state was changed outside {@link com.alexvasilkov.gestures.GesturesController} you should
     * call {@link GesturesController#updateState()} to properly apply changes.
     */
    public State getState() {
        return mState;
    }

    /**
     * Returns state controller that can be used externally.
     */
    public StateController getStateController() {
        return mStateController;
    }

    /**
     * Applies state restrictions and notifies {@link com.alexvasilkov.gestures.GesturesController.OnStateChangedListener}
     * listener.
     */
    public void updateState() {
        mStateController.updateState(mState);
        notifyStateUpdated();
    }

    /**
     * Resets to initial state (default position, min zoom level) and notifies
     * {@link com.alexvasilkov.gestures.GesturesController.OnStateChangedListener} listener.
     * <p/>
     * Should be called after view size is changed.
     * <p/>
     * See {@link com.alexvasilkov.gestures.Settings#setSize(int, int)}.
     */
    public void resetState() {
        mStateController.resetState(mState);
        notifyStateUpdated();
    }

    /**
     * Animates current state to provided end state. Note, that no state restrictions will be applied during animation,
     * so you should ensure end state is within bounds.
     */
    public void animateStateTo(State endState) {
        if (endState == null) return;

        // Ensuring we always starts in correct state
        if (mStateScroller.isFinished()) {
            mStateController.restrictStateBounds(mState, mPrevState, mPivotX, mPivotY, true, true);
        }

        stopFlingAnimation();
        stopStateAnimation();

        mStateStart.set(mState);
        mStateEnd.set(endState);
        mStateScroller.startScroll(0f, 1f);
        mAnimationTick.startAnimation();
    }

    public void stopStateAnimation() {
        mStateScroller.forceFinished(true);
    }

    public void notifyStateUpdated() {
        mPrevState.set(mState);
        mStateListener.onStateChanged(mState);
    }


    // -------------------
    //  Gestures handling
    // -------------------

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        boolean result = mGestureDetector.onTouchEvent(event);
        result |= mScaleDetector.onTouchEvent(event);
        result |= mRotateDetector.onTouchEvent(event);

        if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
            onUpOrCancel(event);
        }

        if (mStateScroller.isFinished()) {
            mStateController.restrictStateBounds(mState, mPrevState, mPivotX, mPivotY, true, true);
        }

        if (!mState.equals(mPrevState)) {
            mPrevState.set(mState);
            notifyStateUpdated();
        }

        return result;
    }

    @Override
    public boolean onDown(MotionEvent e) {
        stopFlingAnimation();

        mIsDoubleTapDetected = false;
        mIsScrollDetected = false;
        mIsFlingDetected = false;
        mIsScaleDetected = false;

        if (mGestureListener != null) mGestureListener.onDown(e);

        if (mSettings.isEnabled()) {
            stopStateAnimation();
            return true;
        } else {
            return false;
        }
    }

    protected void onUpOrCancel(MotionEvent e) {
        if (mIsFlingDetected || mIsDoubleTapDetected) return;

        State endState = mStateController.restrictStateBoundsCopy(mState, mPivotX, mPivotY, false, false);
        animateStateTo(endState);
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return mGestureListener != null && mGestureListener.onSingleTapUp(e);
    }

    @Override
    public void onLongPress(MotionEvent e) {
        if (mGestureListener != null) mGestureListener.onLongPress(e);
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        if (!mSettings.isPanEnabled() || !mStateScroller.isFinished()) return false;

        if (!mIsScrollDetected) {
            mIsScrollDetected = Math.abs(e2.getX() - e1.getX()) > mTouchSlop
                    || Math.abs(e2.getY() - e1.getY()) > mTouchSlop;

            // First scroll event can jerk a bit, so we will ignore it for smoother scrolling
            if (mIsScrollDetected) return true;
        }

        if (mIsScrollDetected) mState.translateBy(-distanceX, -distanceY);

        return mIsScrollDetected;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        if (!mSettings.isPanEnabled() || !mStateScroller.isFinished()) return false;

        mIsFlingDetected = true;

        int x = Math.round(mState.getX());
        int y = Math.round(mState.getY());

        // Fling bounds including current position
        mFlingBounds.set(mStateController.getMovementBounds(mState));
        mFlingBounds.union(x, y);

        stopFlingAnimation();
        mFlingScroller.fling(
                x, y,
                limitFlingVelocity(velocityX * FLING_COEFFICIENT),
                limitFlingVelocity(velocityY * FLING_COEFFICIENT),
                Integer.MIN_VALUE, Integer.MAX_VALUE,
                Integer.MIN_VALUE, Integer.MAX_VALUE);
        mAnimationTick.startAnimation();

        return true;
    }

    private int limitFlingVelocity(float velocity) {
        if (Math.abs(velocity) < mMinimumVelocity) return 0;
        if (Math.abs(velocity) >= mMaximumVelocity) return (int) Math.signum(velocity) * mMaximumVelocity;
        return Math.round(velocity);
    }

    protected void onFlingScroll(float fromX, float fromY, float toX, float toY) {
        float x = toX, y = toY;
        if (mSettings.isRestrictBounds()) {
            PointF pos = mFlingBounds.restrict(x, y);
            x = pos.x;
            y = pos.y;
        }

        mState.translateTo(x, y);
    }

    protected void stopFlingAnimation() {
        mFlingScroller.forceFinished(true);
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return mGestureListener != null && mGestureListener.onSingleTapConfirmed(e);
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        if (e.getActionMasked() != MotionEvent.ACTION_UP) return false;
        // ScaleGestureDetector can perform zoom by "double tap & drag" since KITKAT,
        // so we should suppress our double tap in this case
        if (mIsScaleDetected) return false;

        // Let user redefine double tap
        if (mGestureListener != null && mGestureListener.onDoubleTap(e)) return true;

        if (!mSettings.isDoubleTapEnabled()) return false;

        mIsDoubleTapDetected = true;

        State endState = mStateController.toggleMinMaxZoom(mState, e.getX(), e.getY());
        mStateController.restrictStateBounds(endState, null, e.getX(), e.getY(), false, false);
        animateStateTo(endState);

        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        mIsScaleDetected = mSettings.isZoomEnabled();
        return mIsScaleDetected;
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        if (!mSettings.isZoomEnabled() || !mStateScroller.isFinished()) return true;

        if (detector.getCurrentSpan() > mZoomGestureMinSpan) {
            // When scale is end (in onRotationEnd method),
            // scale detector will return wrong focus point, so we should save it here
            mPivotX = detector.getFocusX();
            mPivotY = detector.getFocusY();
            mState.zoomBy(detector.getScaleFactor(), mPivotX, mPivotY);
        }

        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        mIsScaleDetected = false;

        if (!mSettings.isZoomEnabled()) return;

        // Scroll can still be in place, so we should preserver overscroll
        State endState = mStateController.restrictStateBoundsCopy(mState, mPivotX, mPivotY, true, false);
        animateStateTo(endState);
    }

    @Override
    public boolean onRotationBegin(RotationGestureDetector detector) {
        return mSettings.isRotationEnabled();
    }

    @Override
    public boolean onRotate(RotationGestureDetector detector) {
        if (!mSettings.isRotationEnabled() || !mStateScroller.isFinished()) return true;

        mState.rotateBy(detector.getRotationDelta(), detector.getFocusX(), detector.getFocusY());

        return true;
    }

    protected void onFlingAnimationFinished() {
        State endState = mStateController.restrictStateBoundsCopy(mState, 0f, 0f, false, false);
        animateStateTo(endState);
    }

    protected void onStateAnimationFinished() {
        // no-op
    }

    /**
     * Runnable implementation to animate state changes
     */
    private class AnimationTick implements Runnable {

        private final Handler mHandler = new Handler();

        @Override
        public void run() {
            boolean needsInvalidate = false;

            if (!mFlingScroller.isFinished()) {
                if (mFlingScroller.computeScrollOffset()) {
                    float lastX = mState.getX(), lastY = mState.getY();

                    float x = mFlingScroller.getCurrX();
                    float y = mFlingScroller.getCurrY();

                    onFlingScroll(lastX, lastY, x, y);

                    if (State.equals(lastX, mState.getX()) && State.equals(lastY, mState.getY())) {
                        stopFlingAnimation();
                    }

                    needsInvalidate = true;
                }

                if (mFlingScroller.isFinished()) {
                    onFlingAnimationFinished();
                }
            }

            if (!mStateScroller.isFinished()) {
                mStateScroller.computeScroll();
                float factor = mStateScroller.getCurr();
                StateController.interpolate(mState, mStateStart, mStateEnd, factor);
                needsInvalidate = true;

                if (mStateScroller.isFinished()) {
                    onStateAnimationFinished();
                }
            }

            if (needsInvalidate) {
                notifyStateUpdated();
                startAnimation();
            }
        }

        void startAnimation() {
            mHandler.removeCallbacks(this);
            mHandler.postDelayed(this, 10); // small delay is required (sometimes runnable can be called immediately)
        }

    }


    // -------------------
    //  Listeners
    // -------------------

    /**
     * State changes listener
     */
    public interface OnStateChangedListener {
        void onStateChanged(State state);
    }

    /**
     * Listener for different touch events
     */
    public interface OnGestureListener {
        void onDown(MotionEvent e);

        boolean onSingleTapUp(MotionEvent e);

        void onLongPress(MotionEvent e);

        boolean onSingleTapConfirmed(MotionEvent e);

        boolean onDoubleTap(MotionEvent e);
    }

    /**
     * Simple implementation of {@link com.alexvasilkov.gestures.GesturesController.OnGestureListener}
     */
    public static class SimpleOnGestureListener implements OnGestureListener {
        @Override
        public void onDown(MotionEvent e) {
            // no-op
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            // no-op
        }

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

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            return false;
        }
    }

}




Java Source Code List

com.alexvasilkov.gestures.GesturesAdapter.java
com.alexvasilkov.gestures.GesturesControllerPagerFix.java
com.alexvasilkov.gestures.GesturesController.java
com.alexvasilkov.gestures.Settings.java
com.alexvasilkov.gestures.StateController.java
com.alexvasilkov.gestures.State.java
com.alexvasilkov.gestures.detectors.RotationGestureDetector.java
com.alexvasilkov.gestures.detectors.ScaleGestureDetectorFixed.java
com.alexvasilkov.gestures.sample.activities.BaseActivity.java
com.alexvasilkov.gestures.sample.activities.ImageCroppingActivity.java
com.alexvasilkov.gestures.sample.activities.ImageSnapshotActivity.java
com.alexvasilkov.gestures.sample.activities.ImagesPagerActivity.java
com.alexvasilkov.gestures.sample.activities.LayoutPagerActivity.java
com.alexvasilkov.gestures.sample.activities.MainActivity.java
com.alexvasilkov.gestures.sample.activities.TextViewActivity.java
com.alexvasilkov.gestures.sample.items.Painting.java
com.alexvasilkov.gestures.sample.items.PaintingsImagesAdapter.java
com.alexvasilkov.gestures.sample.items.PaintingsLayoutsAdapter.java
com.alexvasilkov.gestures.sample.utils.PicassoHelper.java
com.alexvasilkov.gestures.utils.FloatScroller.java
com.alexvasilkov.gestures.utils.MovementBounds.java
com.alexvasilkov.gestures.utils.SmoothViewPagerScroller.java
com.alexvasilkov.gestures.utils.Snapshot.java
com.alexvasilkov.gestures.widgets.GestureImageView.java
com.alexvasilkov.gestures.widgets.GestureLayout.java
com.alexvasilkov.gestures.widgets.GestureTextView.java