com.wunderlist.slidinglayer.SlidingLayer.java Source code

Java tutorial

Introduction

Here is the source code for com.wunderlist.slidinglayer.SlidingLayer.java

Source

/*
 * SlidingLayer.java
 * 
 * Copyright (C) 2015 6 Wunderkinder GmbH.
 * 
 * @author      Jose L Ugia - @Jl_Ugia
 * @author      Antonio Consuegra - @aconsuegra
 * @author      Cesar Valiente - @CesarValiente
 * @version     1.2.0
 *
 * 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.wunderlist.slidinglayer;

import java.util.Random;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.Scroller;

public class SlidingLayer extends FrameLayout {

    private static final String STATE_KEY = "state";

    /**
     * Special value for the position of the layer. STICK_TO_RIGHT means that
     * the view shall be attached to the right side of the screen, and come from
     * there into the viewable area.
     */
    public static final int STICK_TO_RIGHT = -1;

    /**
     * Special value for the position of the layer. STICK_TO_LEFT means that the
     * view shall be attached to the left side of the screen, and come from
     * there into the viewable area.
     */
    public static final int STICK_TO_LEFT = -2;

    /**
     * Special value for the position of the layer. STICK_TO_TOP means that the view will stay attached to the top
     * part of the screen, and come from there into the viewable area.
     */
    public static final int STICK_TO_TOP = -3;

    /**
     * Special value for the position of the layer. STICK_TO_BOTTOM means that the view will stay attached to the
     * bottom part of the screen, and come from there into the viewable area.
     */
    public static final int STICK_TO_BOTTOM = -4;

    private static final int HORIZONTAL = 0;
    private static final int VERTICAL = 1;

    private static final int HIGH_VELOCITY = 9000;
    private static final int MAX_SCROLLING_DURATION = 600; // in ms
    private static final int MIN_DISTANCE_FOR_FLING = 10; // in dip
    private static final Interpolator sMenuInterpolator = new Interpolator() {
        @Override
        public float getInterpolation(float t) {
            t -= 1.0f;
            return (float) Math.pow(t, 5) + 1.0f;
        }
    };

    /**
     * Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
     */
    private static final int INVALID_VALUE = -1;
    protected int mActivePointerId = INVALID_VALUE;
    protected VelocityTracker mVelocityTracker;
    protected int mMaximumVelocity;

    private Random mRandom;
    protected Bundle mState;

    private Scroller mScroller;

    private int mShadowSize;
    private Drawable mShadowDrawable;
    private boolean mForceLayout;

    /**
     * The size of the panel that sticks out when closed
     */
    private int mOffsetDistance;

    private boolean mDrawingCacheEnabled;
    private int mScreenSide;

    /**
     * If the user taps the layer then we will switch state it if enabled.
     */
    private boolean changeStateOnTap = true;

    /**
     * The size of the panel in preview mode
     */
    private int mPreviewOffsetDistance = INVALID_VALUE;

    private boolean mEnabled = true;
    private boolean mSlidingFromShadowEnabled = true;
    private boolean mIsDragging;
    private boolean mIsUnableToDrag;
    private int mTouchSlop;

    private float mLastX = INVALID_VALUE;
    private float mLastY = INVALID_VALUE;

    private float mInitialX = INVALID_VALUE;
    private float mInitialY = INVALID_VALUE;
    private float mInitialRawX = INVALID_VALUE;
    private float mInitialRawY = INVALID_VALUE;

    /**
     * Flags to determine the state of the layer
     */
    private static final int STATE_CLOSED = 0;
    private static final int STATE_PREVIEW = 1;
    private static final int STATE_OPENED = 2;

    private int mCurrentState;

    private boolean mScrolling;

    private OnInteractListener mOnInteractListener;

    /**
     * Optional callback to notify client when scroll position has changed
     */
    private OnScrollListener mOnScrollListener;

    private int mMinimumVelocity;
    private int mFlingDistance;

    private LayerTransformer mLayerTransformer;

    public SlidingLayer(Context context) {
        this(context, null);
    }

    public SlidingLayer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    /**
     * Constructor for the sliding layer.<br>
     * By default this panel will
     * <ol>
     * <li>{@link #setStickTo(int)} with param {@link #STICK_TO_RIGHT}</li>
     * <li>Use no shadow drawable. (i.e. with size of 0)</li>
     * <li>Close when the panel is tapped</li>
     * <li>Open when the offset is tapped, but will have an offset of 0</li>
     * </ol>
     *
     * @param context  a reference to an existing context
     * @param attrs    attribute set constructed from attributes set in android .xml file
     * @param defStyle style res id
     */
    public SlidingLayer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        // Style
        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingLayer);

        // Set the side of the screen
        setStickTo(ta.getInt(R.styleable.SlidingLayer_stickTo, STICK_TO_RIGHT));

        // Sets the shadow drawable
        int shadowRes = ta.getResourceId(R.styleable.SlidingLayer_shadowDrawable, INVALID_VALUE);
        if (shadowRes != INVALID_VALUE) {
            setShadowDrawable(shadowRes);
        }

        // Sets the shadow size
        mShadowSize = (int) ta.getDimension(R.styleable.SlidingLayer_shadowSize, 0);

        // Sets the ability to open or close the layer by tapping in any empty space
        changeStateOnTap = ta.getBoolean(R.styleable.SlidingLayer_changeStateOnTap, true);

        // How much of the view sticks out when closed
        mOffsetDistance = ta.getDimensionPixelOffset(R.styleable.SlidingLayer_offsetDistance, 0);

        // Sets the size of the preview summary, if any
        mPreviewOffsetDistance = ta.getDimensionPixelOffset(R.styleable.SlidingLayer_previewOffsetDistance,
                INVALID_VALUE);

        // If showing offset is greater than preview mode offset dimension, exception is thrown
        checkPreviewModeConsistency();

        ta.recycle();

        init();
    }

    private void init() {

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            setLayerType(LAYER_TYPE_HARDWARE, null);
        }

        setWillNotDraw(false);
        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
        setFocusable(true);

        final Context context = getContext();
        mScroller = new Scroller(context, sMenuInterpolator);

        final ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();

        final float density = context.getResources().getDisplayMetrics().density;
        mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);

        mRandom = new Random();
    }

    /**
     * Method exposing the state of the panel
     *
     * @return returns the state of the panel (@link STATE_OPENED, STATE_CLOSED or STATE_PREVIEW). Please note
     * that if the panel was opened with smooth animation this method is not guaranteed to return
     * its final value until the panel has reached its final position.
     */
    private int getCurrentState() {
        return mCurrentState;
    }

    public boolean isOpened() {
        return mCurrentState == STATE_OPENED;
    }

    public boolean isInPreviewMode() {
        return mCurrentState == STATE_PREVIEW;
    }

    public boolean isClosed() {
        return mCurrentState == STATE_CLOSED;
    }

    public void openLayer(final boolean smoothAnimation) {
        setLayerState(STATE_OPENED, smoothAnimation);
    }

    public void openPreview(final boolean smoothAnimation) {
        if (mPreviewOffsetDistance == INVALID_VALUE) {
            throw new IllegalStateException("A value offset for the preview has to be specified in order to open "
                    + "the layer in preview mode. Use setPreviewOffsetDistance or set its associated XML property ");
        }
        setLayerState(STATE_PREVIEW, smoothAnimation);
    }

    public void closeLayer(final boolean smoothAnimation) {
        setLayerState(STATE_CLOSED, smoothAnimation);
    }

    private void setLayerState(final int state, final boolean smoothAnimation) {
        setLayerState(state, smoothAnimation, false);
    }

    private void setLayerState(final int state, final boolean smoothAnimation, boolean force) {
        setLayerState(state, smoothAnimation, force, 0, 0);
    }

    private void setLayerState(final int state, final boolean smoothAnimation, final boolean force,
            final int velocityX, final int velocityY) {

        if (!force && mCurrentState == state) {
            setDrawingCacheEnabled(false);
            return;
        }

        if (mOnInteractListener != null) {
            notifyActionStartedForState(state);
        }

        final int pos[] = getDestScrollPosForState(state);

        if (smoothAnimation) {
            int velocity = allowedDirection() == HORIZONTAL ? velocityX : velocityY;
            smoothScrollTo(pos[0], pos[1], velocity);
        } else {
            completeScroll();
            scrollToAndNotify(pos[0], pos[1]);
        }

        mCurrentState = state;
    }

    /**
     * Sets the listener to be invoked after a switch change
     * {@link OnInteractListener}.
     *
     * @param listener Listener to set
     */
    public void setOnInteractListener(OnInteractListener listener) {
        mOnInteractListener = listener;
    }

    /**
     * Sets the listener to be invoked when the layer is being scrolled
     * {@link OnScrollListener}.
     *
     * @param listener Listener to set
     */
    public void setOnScrollListener(OnScrollListener listener) {
        mOnScrollListener = listener;
    }

    /**
     * Sets the transformer to use when the layer is being scrolled
     * {@link LayerTransformer}.
     *
     * @param layerTransformer Transformer to adopt
     */
    public void setLayerTransformer(LayerTransformer layerTransformer) {
        mLayerTransformer = layerTransformer;
    }

    /**
     * Sets the shadow of the size which will be included within the view by
     * using padding since it's on the left of the view in this case
     *
     * @param shadowSize Desired size of the shadow
     * @see #getShadowSize()
     * @see #setShadowDrawable(Drawable)
     * @see #setShadowDrawable(int)
     */
    public void setShadowSize(final int shadowSize) {
        mShadowSize = shadowSize;
        invalidate(getLeft(), getTop(), getRight(), getBottom());
    }

    /**
     * Sets the shadow size by the value of a resource.
     *
     * @param resId The dimension resource id to be set as the shadow size.
     */
    public void setShadowSizeRes(int resId) {
        setShadowSize((int) getResources().getDimension(resId));
    }

    /**
     * Return the current size of the shadow.
     *
     * @return The size of the shadow in pixels
     */
    public int getShadowSize() {
        return mShadowSize;
    }

    /**
     * Sets a drawable that will be used to create the shadow for the layer.
     *
     * @param d Drawable append as a shadow
     */
    public void setShadowDrawable(final Drawable d) {
        mShadowDrawable = d;
        refreshDrawableState();
        setWillNotDraw(false);
        invalidate(getLeft(), getTop(), getRight(), getBottom());
    }

    /**
     * Sets a drawable resource that will be used to create the shadow for the
     * layer.
     *
     * @param resId Resource ID of a drawable
     */
    public void setShadowDrawable(int resId) {
        setShadowDrawable(getContext().getResources().getDrawable(resId));
    }

    /**
     * Sets the offset distance of the panel by using a dimension resource.
     *
     * @param resId The dimension resource id to be set as the offset.
     */
    public void setOffsetDistanceRes(int resId) {
        setOffsetDistance((int) getResources().getDimension(resId));
    }

    /**
     * Sets the offset distance of the panel. How much sticks out when off screen.
     *
     * @param offsetDistance Size of the offset in pixels
     * @see #getOffsetDistance()
     */
    public void setOffsetDistance(int offsetDistance) {
        mOffsetDistance = offsetDistance;
        checkPreviewModeConsistency();
        invalidate(getLeft(), getTop(), getRight(), getBottom());
    }

    /**
     * @return returns the number of pixels that are visible when the panel is closed
     */
    public int getOffsetDistance() {
        return mOffsetDistance;
    }

    /**
     * Sets the offset distance of the preview panel by using a dimension resource.
     *
     * @param resId The dimension resource id to be set as the size of the preview mode.
     */
    public void setPreviewOffsetDistanceRes(int resId) {
        setPreviewOffsetDistance((int) getResources().getDimension(resId));
    }

    /**
     * Sets the size of the panel when in preview mode.
     *
     * @param previewOffsetDistance Size of the offset in pixels
     * @see #getOffsetDistance()
     */
    public void setPreviewOffsetDistance(int previewOffsetDistance) {
        mPreviewOffsetDistance = previewOffsetDistance;

        checkPreviewModeConsistency();
        invalidate(getLeft(), getTop(), getRight(), getBottom());

        if (mCurrentState == STATE_PREVIEW) {
            smoothScrollToCurrentPosition();
        }
    }

    private void checkPreviewModeConsistency() {
        if (isPreviewModeEnabled() && mOffsetDistance > mPreviewOffsetDistance) {
            throw new IllegalStateException("The showing offset of the layer can never be greater than the "
                    + "offset dimension of the preview mode");
        }
    }

    /**
     * @return true if the preview mode is enabled
     */
    private boolean isPreviewModeEnabled() {
        return mPreviewOffsetDistance != INVALID_VALUE;
    }

    @Override
    protected boolean verifyDrawable(Drawable who) {
        return super.verifyDrawable(who) || who == mShadowDrawable;
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        final Drawable d = mShadowDrawable;
        if (d != null && d.isStateful()) {
            d.setState(getDrawableState());
        }
    }

    public boolean isSlidingEnabled() {
        return mEnabled;
    }

    public void setSlidingEnabled(boolean _enabled) {
        mEnabled = _enabled;
    }

    public boolean isSlidingFromShadowEnabled() {
        return mSlidingFromShadowEnabled;
    }

    public void setSlidingFromShadowEnabled(boolean _slidingShadow) {
        mSlidingFromShadowEnabled = _slidingShadow;
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState state = new SavedState(superState);
        if (mState == null) {
            mState = new Bundle();
        }
        mState.putInt(STATE_KEY, mCurrentState);
        state.mState = mState;
        return state;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        restoreState(savedState.mState);
    }

    public void restoreState(Parcelable in) {
        mState = (Bundle) in;
        int state = mState.getInt(STATE_KEY);
        setLayerState(state, true);
    }

    private float getViewX(MotionEvent event) {
        return event.getRawX();
    }

    private float getViewY(MotionEvent event) {
        return event.getRawY();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        if (!mEnabled) {
            return false;
        }

        final int action = ev.getAction() & MotionEvent.ACTION_MASK;

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            mIsDragging = false;
            mIsUnableToDrag = false;
            mActivePointerId = INVALID_VALUE;
            if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
            }
            return false;
        }

        if (action != MotionEvent.ACTION_DOWN) {
            if (mIsDragging) {
                return true;
            } else if (mIsUnableToDrag) {
                return false;
            }
        }

        switch (action) {
        case MotionEvent.ACTION_MOVE:

            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_VALUE) {
                break;
            }
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            final float x = getViewX(ev);
            final float dx = x - mLastX;
            final float xDiff = Math.abs(dx);
            final float y = getViewY(ev);
            final float dy = y - mLastY;
            final float yDiff = Math.abs(dy);

            if ((dx != 0 || dy != 0) && canScroll(this, false, (int) dx, (int) dy, (int) x, (int) y)) {
                mLastX = mInitialRawX = x;
                mLastY = mInitialRawY = y;
                mInitialX = ev.getX(pointerIndex);
                mInitialY = ev.getY(pointerIndex);
                return false;
            }

            final boolean validHorizontalDrag = xDiff > mTouchSlop && xDiff > yDiff;
            final boolean validVerticalDrag = yDiff > mTouchSlop && yDiff > xDiff;

            if (validHorizontalDrag) {
                mLastX = x;
            } else if (validVerticalDrag) {
                mLastY = y;
            }

            if (validHorizontalDrag || validVerticalDrag) {
                mIsDragging = true;
                setDrawingCacheEnabled(true);
            }
            break;

        case MotionEvent.ACTION_DOWN:
            mLastX = mInitialRawX = getViewX(ev);
            mLastY = mInitialRawY = getViewY(ev);
            mInitialX = ev.getX(0);
            mInitialY = ev.getY(0);
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);

            if (touchPointIsWithinBounds(ev.getX(), ev.getY())) {
                mIsDragging = false;
                mIsUnableToDrag = false;
                // We don't want to do anything, send the event up
                return super.onInterceptTouchEvent(ev);
            } else {
                completeScroll();
                mIsDragging = false;
                mIsUnableToDrag = true;
            }
            break;
        case MotionEvent.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
        }

        if (!mIsDragging) {
            if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.addMovement(ev);
        }

        return mIsDragging;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {

        if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
            return false;
        }

        if (!mEnabled || !mIsDragging && !touchPointIsWithinBounds(mInitialX, mInitialY)) {
            return false;
        }

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

        final int action = ev.getAction();

        switch (action & MotionEvent.ACTION_MASK) {
        case MotionEvent.ACTION_DOWN: {
            completeScroll();

            // Remember where the motion event started
            mLastX = mInitialRawX = getViewX(ev);
            mLastY = mInitialRawY = getViewY(ev);
            mInitialX = ev.getX();
            mInitialY = ev.getY();
            mActivePointerId = ev.getPointerId(0);
            break;
        }

        case MotionEvent.ACTION_MOVE: {

            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);

            if (!touchPointIsWithinBounds(ev.getX(), ev.getY(), false))
                return false;

            final float x = getViewX(ev);
            final float y = getViewY(ev);

            final float deltaX = mLastX - x;
            final float deltaY = mLastY - y;

            mLastX = x;
            mLastY = y;

            if (!mIsDragging) {

                final float xDiff = Math.abs(x - mInitialRawX);
                final float yDiff = Math.abs(y - mInitialRawY);

                final boolean validHorizontalDrag = xDiff > mTouchSlop && xDiff > yDiff;
                final boolean validVerticalDrag = yDiff > mTouchSlop && yDiff > xDiff;

                if (validHorizontalDrag || validVerticalDrag) {
                    mIsDragging = true;
                    setDrawingCacheEnabled(true);
                }
            }

            if (mIsDragging) {

                final float oldScrollX = getScrollX();
                final float oldScrollY = getScrollY();
                float scrollX = oldScrollX + deltaX;
                float scrollY = oldScrollY + deltaY;

                // Log.d("Layer", String.format("Layer scrollX[%f],scrollY[%f]", scrollX, scrollY));
                final float leftBound, rightBound;
                final float bottomBound, topBound;
                switch (mScreenSide) {
                case STICK_TO_LEFT:
                    topBound = bottomBound = rightBound = 0;
                    leftBound = getWidth(); // How far left we can scroll
                    break;
                case STICK_TO_RIGHT:
                    rightBound = -getWidth();
                    topBound = bottomBound = leftBound = 0;
                    break;
                case STICK_TO_TOP:
                    topBound = getHeight();
                    bottomBound = rightBound = leftBound = 0;
                    break;
                case STICK_TO_BOTTOM:
                    bottomBound = -getHeight();
                    topBound = rightBound = leftBound = 0;
                    break;
                default:
                    topBound = bottomBound = rightBound = leftBound = 0;
                    break;
                }

                if (scrollX > leftBound) {
                    scrollX = leftBound;
                } else if (scrollX < rightBound) {
                    scrollX = rightBound;
                }
                if (scrollY > topBound) {
                    scrollY = topBound;
                } else if (scrollY < bottomBound) {
                    scrollY = bottomBound;
                }

                // Keep the precision
                mLastX += scrollX - (int) scrollX;
                mLastY += scrollY - (int) scrollY;

                scrollToAndNotify((int) scrollX, (int) scrollY);
            }
            break;
        }

        case MotionEvent.ACTION_UP: {

            if (mIsDragging) {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                final int initialVelocityX = (int) VelocityTrackerCompat.getXVelocity(velocityTracker,
                        mActivePointerId);
                final int initialVelocityY = (int) VelocityTrackerCompat.getYVelocity(velocityTracker,
                        mActivePointerId);
                final int scrollX = getScrollX();
                final int scrollY = getScrollY();

                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                final float x = getViewX(ev);
                final float y = getViewY(ev);

                int nextState = determineNextStateForDrag(scrollX, scrollY, initialVelocityX, initialVelocityY,
                        (int) mInitialRawX, (int) mInitialRawY, (int) x, (int) y);
                setLayerState(nextState, true, true, initialVelocityX, initialVelocityY);

                mActivePointerId = INVALID_VALUE;
                endDrag();

            } else if (changeStateOnTap) {
                int nextState = determineNextStateAfterTap();
                setLayerState(nextState, true, true);
            }
            break;
        }

        case MotionEvent.ACTION_CANCEL:
            if (mIsDragging) {
                setLayerState(mCurrentState, true, true);
                mActivePointerId = INVALID_VALUE;
                endDrag();
            }
            break;

        case MotionEvent.ACTION_POINTER_DOWN: {
            final int pointerIndex = MotionEventCompat.getActionIndex(ev);
            mActivePointerId = ev.getPointerId(pointerIndex);
            mLastX = getViewX(ev);
            mLastY = getViewY(ev);
            break;

        }
        case MotionEvent.ACTION_POINTER_UP: {
            onSecondaryPointerUp(ev);
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            mLastX = getViewX(ev);
            mLastY = getViewY(ev);
            break;
        }
        }

        return true;
    }

    /**
     * Checks if it's allowed to slide from the given position.
     *
     * @param touchX where the touch event started
     * @param touchY where the touch event started.
     * @return true if you can drag this view, false otherwise
     */
    private boolean touchPointIsWithinBounds(final float touchX, final float touchY) {
        return touchPointIsWithinBounds(touchX, touchY, true);
    }

    private boolean touchPointIsWithinBounds(final float touchX, final float touchY, boolean withinLayer) {

        int scroll = 0;
        float touch;

        if (allowedDirection() == HORIZONTAL) {
            if (withinLayer)
                scroll = getScrollX();
            touch = touchX;
        } else {
            if (withinLayer)
                scroll = getScrollY();
            touch = touchY;
        }

        switch (mScreenSide) {
        case STICK_TO_RIGHT:
        case STICK_TO_BOTTOM:
            return touch >= -scroll;
        case STICK_TO_LEFT:
            return touch <= getWidth() - scroll;
        case STICK_TO_TOP:
            return touch <= getHeight() - scroll;
        default:
            throw new IllegalStateException("The layer has to be stuck to one of the sides of the screen. "
                    + "Current value is: " + mScreenSide);
        }
    }

    protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) {

        if (v instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = v.getScrollX();
            final int scrollY = v.getScrollY();

            final int count = group.getChildCount();
            // Count backwards - let topmost views consume scroll distance first.
            for (int i = count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child = group.getChildAt(i);
                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight()
                        && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child,
                                true, dx, dy, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        return checkV && ((allowedDirection() == HORIZONTAL && ViewCompat.canScrollHorizontally(v, -dx)
                || allowedDirection() == VERTICAL && ViewCompat.canScrollVertically(v, -dy)));
    }

    /**
     * Based on the position and velocity of the layer we calculate what the next state should be.
     *
     * @param velocityX
     * @param velocityY
     * @param initialX
     * @param initialY
     * @param currentX
     * @param currentY
     * @return the state of the panel (@link STATE_OPENED, STATE_CLOSED or STATE_PREVIEW).
     */
    private int determineNextStateForDrag(final int scrollX, final int scrollY, final int velocityX,
            final int velocityY, final int initialX, final int initialY, final int currentX, final int currentY) {

        int panelOffset;
        int panelSize;
        int relativeVelocity;
        int absoluteDelta;

        if (allowedDirection() == HORIZONTAL) {
            panelSize = getWidth();
            panelOffset = Math.abs(panelSize - Math.abs(scrollX));
            absoluteDelta = Math.abs(currentX - initialX);
            relativeVelocity = velocityX * (mScreenSide == STICK_TO_LEFT ? 1 : -1);
        } else {
            panelSize = getHeight();
            panelOffset = Math.abs(panelSize - Math.abs(scrollY));
            absoluteDelta = Math.abs(currentY - initialY);
            relativeVelocity = velocityY * (mScreenSide == STICK_TO_TOP ? 1 : -1);
        }

        final int absoluteVelocity = Math.abs(relativeVelocity);
        final boolean isOverThreshold = absoluteDelta > mFlingDistance && absoluteVelocity > mMinimumVelocity;

        if (isOverThreshold) {

            if (relativeVelocity > 0) {
                return STATE_OPENED;
            } else {

                boolean goesToPreview = isPreviewModeEnabled() && panelOffset > mPreviewOffsetDistance
                        && absoluteVelocity < HIGH_VELOCITY;

                if (goesToPreview) {
                    return STATE_PREVIEW;
                } else {
                    return STATE_CLOSED;
                }
            }

        } else {

            int openedThreshold = (panelSize + (isPreviewModeEnabled() ? mPreviewOffsetDistance : 0)) / 2;

            if (panelOffset > openedThreshold) {
                return STATE_OPENED;
            } else if (isPreviewModeEnabled() && panelOffset > mPreviewOffsetDistance / 2) {
                return STATE_PREVIEW;
            } else {
                return STATE_CLOSED;
            }
        }
    }

    /**
     * Based on the current state of the panel, this method returns the next state after tapping.
     *
     * @return the state of the panel (@link STATE_OPENED, STATE_CLOSED or STATE_PREVIEW).
     */
    private int determineNextStateAfterTap() {

        switch (mCurrentState) {
        case STATE_CLOSED:
            return isPreviewModeEnabled() ? STATE_PREVIEW : STATE_OPENED;
        case STATE_PREVIEW:
            return STATE_OPENED;
        case STATE_OPENED:
            return isPreviewModeEnabled() ? STATE_PREVIEW : STATE_CLOSED;
        }

        return STATE_CLOSED;
    }

    /**
     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
     *
     * @param x the number of pixels to scroll by on the X axis
     * @param y the number of pixels to scroll by on the Y axis
     */
    void smoothScrollTo(int x, int y) {
        smoothScrollTo(x, y, 0);
    }

    /**
     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
     *
     * @param x        the number of pixels to scroll by on the X axis
     * @param y        the number of pixels to scroll by on the Y axis
     * @param velocity the velocity associated with a fling, if applicable. (0
     *                 otherwise)
     */
    void smoothScrollTo(int x, int y, int velocity) {

        if (getChildCount() == 0) {
            setDrawingCacheEnabled(false);
            return;
        }

        int sx = getScrollX();
        int sy = getScrollY();
        int dx = x - sx;
        int dy = y - sy;
        if (dx == 0 && dy == 0) {
            completeScroll();
            if (mOnInteractListener != null) {
                notifyActionFinished();
            }
            return;
        }

        setDrawingCacheEnabled(true);
        mScrolling = true;

        final int width = getWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);

        int duration;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            duration = MAX_SCROLLING_DURATION;
        }
        duration = Math.min(duration, MAX_SCROLLING_DURATION);

        mScroller.startScroll(sx, sy, dx, dy, duration);
        ViewCompat.postInvalidateOnAnimation(this);
    }

    private void smoothScrollToCurrentPosition() {
        int[] pos = getDestScrollPosForState(mCurrentState);
        smoothScrollTo(pos[0], pos[1]);
    }

    // We want the duration of the page snap animation to be influenced by the
    // distance that
    // the screen has to travel, however, we don't want this duration to be
    // effected in a
    // purely linear fashion. Instead, we use this method to moderate the effect
    // that the distance
    // of travel has on the overall snap duration.
    float distanceInfluenceForSnapDuration(float f) {
        f -= 0.5f; // center the values about 0.
        f *= 0.3f * Math.PI / 2.0f;
        return (float) Math.sin(f);
    }

    private void endDrag() {
        mIsDragging = false;
        mIsUnableToDrag = false;

        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    @Override
    public void setDrawingCacheEnabled(boolean enabled) {

        if (mDrawingCacheEnabled != enabled) {
            super.setDrawingCacheEnabled(enabled);
            mDrawingCacheEnabled = enabled;

            final int l = getChildCount();
            for (int i = 0; i < l; i++) {
                final View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    child.setDrawingCacheEnabled(enabled);
                }
            }
        }
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = ev.getActionIndex();
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            // This was our active pointer going up. Choose a new
            // active pointer and adjust accordingly.
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastX = ev.getX(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }

    private void completeScroll() {

        boolean needPopulate = mScrolling;
        if (needPopulate) {
            // Done with scroll, no longer want to cache view drawing.
            setDrawingCacheEnabled(false);
            mScroller.abortAnimation();
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            if (oldX != x || oldY != y) {
                scrollToAndNotify(x, y);
            }
            if (mOnInteractListener != null) {
                notifyActionFinished();
            }
        }
        mScrolling = false;
    }

    private void scrollToAndNotify(int x, int y) {

        scrollTo(x, y);

        if (mOnScrollListener == null && mLayerTransformer == null) {
            return;
        }

        int scroll;
        if (allowedDirection() == VERTICAL) {
            scroll = getHeight() - Math.abs(y);
        } else {
            scroll = getWidth() - Math.abs(x);
        }

        if (mOnScrollListener != null) {
            mOnScrollListener.onScroll(Math.abs(scroll));
        }

        if (mLayerTransformer != null) {

            int absoluteScroll = Math.abs(scroll);
            int layerSize = allowedDirection() == HORIZONTAL ? getMeasuredWidth() : getMeasuredHeight();

            float layerProgress = (float) absoluteScroll / layerSize;
            float previewProgress = mPreviewOffsetDistance > 0
                    ? Math.min(1, (float) absoluteScroll / mPreviewOffsetDistance)
                    : 0;

            mLayerTransformer.internalTransform(this, previewProgress, layerProgress, mScreenSide);
        }
    }

    /**
     * Sets the default location where the SlidingLayer will appear
     *
     * @param screenSide The location where the Sliding layer will appear. Possible values are
     *                   {@link #STICK_TO_BOTTOM}, {@link #STICK_TO_LEFT}
     *                   {@link #STICK_TO_RIGHT}, {@link #STICK_TO_TOP}
     */
    public void setStickTo(int screenSide) {
        mForceLayout = true;
        mScreenSide = screenSide;
        setLayerState(STATE_CLOSED, false, true);
    }

    private int allowedDirection() {

        if (mScreenSide == STICK_TO_TOP || mScreenSide == STICK_TO_BOTTOM) {
            return VERTICAL;
        } else if (mScreenSide == STICK_TO_LEFT || mScreenSide == STICK_TO_RIGHT) {
            return HORIZONTAL;
        }

        throw new IllegalStateException("The screen side of the layer is illegal");
    }

    /**
     * Sets the behavior when tapping the sliding layer
     *
     * @param changeStateOnTap
     */
    public void setChangeStateOnTap(boolean changeStateOnTap) {
        this.changeStateOnTap = changeStateOnTap;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int width = getDefaultSize(0, widthMeasureSpec);
        int height = getDefaultSize(0, heightMeasureSpec);
        setMeasuredDimension(width, height);

        if (mLayerTransformer != null) {
            mLayerTransformer.onMeasure(this, mScreenSide);
        }

        super.onMeasure(getChildMeasureSpec(widthMeasureSpec, 0, width),
                getChildMeasureSpec(heightMeasureSpec, 0, height));
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        boolean scrollMustChange = false;
        if (allowedDirection() == VERTICAL) {
            if (h != oldh) {
                scrollMustChange = true;
            }
        } else if (w != oldw) {
            scrollMustChange = true;
        }

        if (scrollMustChange) {
            completeScroll();
            int[] pos = getDestScrollPosForState(mCurrentState);
            scrollTo(pos[0], pos[1]);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

        if (mForceLayout) {
            mForceLayout = false;
            adjustLayoutParams();

            if (mScreenSide == STICK_TO_RIGHT) {
                setPadding(getPaddingLeft() + mShadowSize, getPaddingTop(), getPaddingRight(), getPaddingBottom());
            } else if (mScreenSide == STICK_TO_BOTTOM) {
                setPadding(getPaddingLeft(), getPaddingTop() + mShadowSize, getPaddingRight(), getPaddingBottom());
            } else if (mScreenSide == STICK_TO_LEFT) {
                setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight() + mShadowSize, getPaddingBottom());
            } else if (mScreenSide == STICK_TO_TOP) {
                setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom() + mShadowSize);
            }
        }

        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    private void adjustLayoutParams() {

        ViewGroup.LayoutParams baseParams = getLayoutParams();

        if (baseParams instanceof LayoutParams) {

            LayoutParams layoutParams = (LayoutParams) baseParams;

            switch (mScreenSide) {
            case STICK_TO_BOTTOM:
                layoutParams.gravity = Gravity.BOTTOM;
                break;
            case STICK_TO_LEFT:
                layoutParams.gravity = Gravity.LEFT;
                break;
            case STICK_TO_RIGHT:
                layoutParams.gravity = Gravity.RIGHT;
                break;
            case STICK_TO_TOP:
                layoutParams.gravity = Gravity.TOP;
                break;
            }
            setLayoutParams(baseParams);

        } else if (baseParams instanceof RelativeLayout.LayoutParams) {

            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) baseParams;

            switch (mScreenSide) {
            case STICK_TO_BOTTOM:
                layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
                break;
            case STICK_TO_LEFT:
                layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
                break;
            case STICK_TO_RIGHT:
                layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
                break;
            case STICK_TO_TOP:
                layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
                break;
            }
        }

    }

    /**
     * Get the destination position based on the velocity
     *
     * @return
     * @since 1.0
     */
    private int[] getDestScrollPosForState(int state) {

        int[] pos = new int[2];

        if (state == STATE_OPENED) {
            return pos;
        } else {

            int layerOffset = state == STATE_CLOSED ? mOffsetDistance : mPreviewOffsetDistance;

            switch (mScreenSide) {
            case STICK_TO_RIGHT:
                pos[0] = -getWidth() + layerOffset;
                break;
            case STICK_TO_LEFT:
                pos[0] = getWidth() - layerOffset;
                break;
            case STICK_TO_TOP:
                pos[1] = getHeight() - layerOffset;
                break;
            case STICK_TO_BOTTOM:
                pos[1] = -getHeight() + layerOffset;
                break;
            }

            return pos;
        }
    }

    public int getContentLeft() {
        return getLeft() + getPaddingLeft();
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        // Draw the margin drawable if needed.
        if (mShadowSize > 0 && mShadowDrawable != null) {
            if (mScreenSide == STICK_TO_RIGHT) {
                mShadowDrawable.setBounds(0, 0, mShadowSize, getHeight());
            }
            if (mScreenSide == STICK_TO_TOP) {
                mShadowDrawable.setBounds(0, getHeight() - mShadowSize, getWidth(), getHeight());
            }
            if (mScreenSide == STICK_TO_LEFT) {
                mShadowDrawable.setBounds(getWidth() - mShadowSize, 0, getWidth(), getHeight());
            }
            if (mScreenSide == STICK_TO_BOTTOM) {
                mShadowDrawable.setBounds(0, 0, getWidth(), mShadowSize);
            }
            mShadowDrawable.draw(canvas);
        }
    }

    @Override
    public void computeScroll() {
        if (!mScroller.isFinished()) {
            if (mScroller.computeScrollOffset()) {
                final int oldX = getScrollX();
                final int oldY = getScrollY();
                final int x = mScroller.getCurrX();
                final int y = mScroller.getCurrY();

                if (oldX != x || oldY != y) {
                    scrollToAndNotify(x, y);
                }

                // Keep on drawing until the animation has finished.
                ViewCompat.postInvalidateOnAnimation(this);
                return;
            }
        }

        // Done with scroll, clean up state.
        completeScroll();
    }

    /**
     * Handler interface for obtaining updates on the <code>SlidingLayer</code>'s state.
     * <code>OnInteractListener</code> allows for external classes to be notified when the <code>SlidingLayer</code>
     * receives input to be opened or closed.
     */
    public interface OnInteractListener {

        /**
         * This method is called when an attempt is made to open the current <code>SlidingLayer</code>. Note
         * that because of animation, the <code>SlidingLayer</code> may not be visible yet.
         */
        void onOpen();

        /**
         * This method is called when an attempt is made to show the preview mode in the current
         * <code>SlidingLayer</code>. Note that because of animation, the <code>SlidingLayer</code> may not be
         * visible yet.
         */
        void onShowPreview();

        /**
         * This method is called when an attempt is made to close the current <code>SlidingLayer</code>. Note
         * that because of animation, the <code>SlidingLayer</code> may still be visible.
         */
        void onClose();

        /**
         * this method is executed after <code>onOpen()</code>, when the animation has finished.
         */
        void onOpened();

        /**
         * this method is executed after <code>onShowPreview()</code>, when the animation has finished.
         */
        void onPreviewShowed();

        /**
         * this method is executed after <code>onClose()</code>, when the animation has finished and the
         * <code>SlidingLayer</code> is
         * therefore no longer visible.
         */
        void onClosed();
    }

    private void notifyActionStartedForState(int state) {

        switch (state) {
        case STATE_CLOSED:
            mOnInteractListener.onClose();
            break;

        case STATE_PREVIEW:
            mOnInteractListener.onShowPreview();
            break;

        case STATE_OPENED:
            mOnInteractListener.onOpen();
            break;
        }
    }

    private void notifyActionFinished() {

        switch (mCurrentState) {
        case STATE_CLOSED:
            mOnInteractListener.onClosed();
            break;

        case STATE_PREVIEW:
            mOnInteractListener.onPreviewShowed();
            break;

        case STATE_OPENED:
            mOnInteractListener.onOpened();
            break;
        }
    }

    /**
     * Interface definition for a callback to be invoked when the layer has been scrolled.
     */
    public interface OnScrollListener {

        /**
         * Callback method to be invoked when the layer has been scrolled. This will be
         * called after the scroll has completed
         *
         * @param absoluteScroll The absolute scrolling delta relative to the position of the container
         */
        void onScroll(int absoluteScroll);
    }

    static class SavedState extends BaseSavedState {

        Bundle mState;

        public SavedState(Parcelable superState) {
            super(superState);
        }

        public SavedState(Parcel in) {
            super(in);
            mState = in.readBundle();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeBundle(mState);
        }

        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}