/*
* SlidingLayer.java
*
* Copyright (C) 2013 6 Wunderkinder GmbH.
*
* @author Jose L Ugia - @Jl_Ugia
* @author Antonio Consuegra - @aconsuegra
* @author Cesar Valiente - @CesarValiente
* @version 1.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.slidinglayer;
import java.lang.reflect.Method;
import java.util.Random;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.util.AttributeSet;
import android.util.FloatMath;
import android.view.Display;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.Scroller;
public class SlidingLayer extends FrameLayout {
/**
* Default value for the position of the layer. STICK_TO_AUTO shall inspect the container and choose a stick
* mode depending on the position of the layout (ie.: layout is positioned on the right = STICK_TO_RIGHT).
*/
public static final int STICK_TO_AUTO = 0;
/**
* 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_MIDDLE means that the view will stay attached trying to
* be in the middle of the screen and allowing dismissing both to right and left side.
*/
public static final int STICK_TO_MIDDLE = -3;
/**
* 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 = -4;
/**
* 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 = -5;
private static final int MAX_SCROLLING_DURATION = 600; // in ms
private static final int MIN_DISTANCE_FOR_FLING = 25; // 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_POINTER = -1;
protected int mActivePointerId = INVALID_POINTER;
protected VelocityTracker mVelocityTracker;
protected int mMaximumVelocity;
private Random mRandom;
private Scroller mScroller;
private int mShadowWidth;
private Drawable mShadowDrawable;
private boolean mForceLayout;
/**
* The with of the panel when closed
*/
private int mOffsetWidth;
private boolean mDrawingCacheEnabled;
private int mScreenSide = STICK_TO_AUTO;
/**
* If the user taps the layer then we will close it if enabled.
*/
private boolean closeOnTapEnabled = true;
/**
* If the user taps the offset then we will open it if enabled.
*/
private boolean openOnTapEnabled = true;
private boolean mEnabled = true;
private boolean mSlidingFromShadowEnabled = true;
private boolean mIsDragging;
private boolean mIsUnableToDrag;
private int mTouchSlop;
private float mLastX = -1;
private float mLastY = -1;
private float mInitialX = -1;
private float mInitialY = -1;
private boolean mIsOpen;
private boolean mScrolling;
private OnInteractListener mOnInteractListener;
private int mMinimumVelocity;
private int mFlingDistance;
private boolean mLastTouchAllowed = false;
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_AUTO}</li>
* <li>Use no shadow drawable. (i.e. with width 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_AUTO));
// Sets the shadow drawable
int shadowRes = ta.getResourceId(R.styleable.SlidingLayer_shadowDrawable, -1);
if (shadowRes != -1) {
setShadowDrawable(shadowRes);
}
// Sets the shadow width
setShadowWidth((int) ta.getDimension(R.styleable.SlidingLayer_shadowWidth, 0));
// Sets the ability to close the layer by tapping in any empty space
closeOnTapEnabled = ta.getBoolean(R.styleable.SlidingLayer_closeOnTapEnabled, true);
// Sets the ability to open the layout by tapping on any of the exposed closed layer
openOnTapEnabled = ta.getBoolean(R.styleable.SlidingLayer_openOnTapEnabled, true);
// How much of the view sticks out when closed
setOffsetWidth(ta.getDimensionPixelOffset(R.styleable.SlidingLayer_offsetWidth, 0));
ta.recycle();
init();
}
private void init() {
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();
}
/**
* Returns whether the panel is open or not.
* @return returns true if the panel is open, false otherwise. Please note that if
* the panel was opened with smooth animation this method is not guaranteed to return
* true. This method will only return true after the panel has completely opened.
*/
public boolean isOpened() {
return mIsOpen;
}
public void openLayer(boolean smoothAnim) {
openLayer(smoothAnim, false);
}
private void openLayer(boolean smoothAnim, boolean forceOpen) {
switchLayer(true, smoothAnim, forceOpen, 0, 0);
}
public void closeLayer(boolean smoothAnim) {
closeLayer(smoothAnim, false);
}
private void closeLayer(boolean smoothAnim, boolean forceClose) {
switchLayer(false, smoothAnim, forceClose, 0, 0);
}
private void switchLayer(boolean open, boolean smoothAnim, boolean forceSwitch) {
switchLayer(open, smoothAnim, forceSwitch, 0, 0);
}
private void switchLayer(final boolean open, final boolean smoothAnim, final boolean forceSwitch,
final int velocityX, final int velocityY) {
if (!forceSwitch && open == mIsOpen) {
setDrawingCacheEnabled(false);
return;
}
if (open) {
if (mOnInteractListener != null) {
mOnInteractListener.onOpen();
}
} else {
if (mOnInteractListener != null) {
mOnInteractListener.onClose();
}
}
mIsOpen = open;
// Get translation values
float tx = mLastX - getWidth() / 2;
float ty = mLastY - getHeight() / 2;
// Get boolean for velocity check
boolean noVelocityInStickToMidle = mScreenSide == STICK_TO_MIDDLE && Math.abs(velocityX) < mMinimumVelocity
&& Math.abs(velocityY) < mMinimumVelocity;
// Follow velocity or translation depending on the case
int dx = noVelocityInStickToMidle ? (int) tx : velocityX;
int dy = noVelocityInStickToMidle ? (int) ty : velocityY;
final int pos[] = getDestScrollPos(dx, dy);
if (smoothAnim) {
smoothScrollTo(pos[0], pos[1], Math.max(velocityX, velocityY));
} else {
completeScroll();
scrollTo(pos[0], pos[1]);
}
}
/**
* 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 shadow width by the value of a resource.
*
* @param resId
* The dimension resource id to be set as the shadow width.
*/
public void setShadowWidthRes(int resId) {
setShadowWidth((int) getResources().getDimension(resId));
}
/**
* Return the current with of the shadow.
*
* @return The size of the shadow in pixels
*/
public int getShadowWidth() {
return mShadowWidth;
}
/**
* Sets the shadow of the width which will be included within the view by using padding since it's on the left
* of the view in this case
*
* @param shadowWidth
* Desired width of the shadow
* @see #getShadowWidth()
* @see #setShadowDrawable(Drawable)
* @see #setShadowDrawable(int)
*/
public void setShadowWidth(final int shadowWidth) {
mShadowWidth = shadowWidth;
invalidate(getLeft(), getTop(), getRight(), getBottom());
}
/**
* 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 width of the panel. How much sticks out when off screen.
*
* @param offsetWidth
* Width of the offset in pixels
* @see #getOffsetWidth()
*/
public void setOffsetWidth(int offsetWidth) {
mOffsetWidth = offsetWidth;
invalidate(getLeft(), getTop(), getRight(), getBottom());
}
/**
*
* @return returns the number of pixels that are visible when the panel is closed
*/
public int getOffsetWidth() {
return mOffsetWidth;
}
@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
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mEnabled) {
return false;
}
final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsDragging = false;
mIsUnableToDrag = false;
mActivePointerId = INVALID_POINTER;
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_POINTER) {
break;
}
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
if (pointerIndex == -1) {
mActivePointerId = INVALID_POINTER;
break;
}
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float dx = x - mLastX;
final float xDiff = Math.abs(dx);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float dy = y - mLastY;
final float yDiff = Math.abs(y - mLastY);
if (xDiff > mTouchSlop && xDiff > yDiff && allowDragingX(dx, mInitialX)) {
mIsDragging = true;
mLastX = x;
setDrawingCacheEnabled(true);
} else if (yDiff > mTouchSlop && yDiff > xDiff && allowDragingY(dy, mInitialY)) {
mIsDragging = true;
mLastY = y;
setDrawingCacheEnabled(true);
}
break;
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getAction()
& (Build.VERSION.SDK_INT >= 8 ? MotionEvent.ACTION_POINTER_INDEX_MASK
: MotionEventCompat.ACTION_POINTER_INDEX_MASK);
mLastX = mInitialX = MotionEventCompat.getX(ev, mActivePointerId);
mLastY = mInitialY = MotionEventCompat.getY(ev, mActivePointerId);
if (allowSlidingFromHereX(ev, mInitialX)) {
mIsDragging = false;
mIsUnableToDrag = false;
// If nobody else got the focus we use it to close the layer
return super.onInterceptTouchEvent(ev);
} else if (allowSlidingFromHereY(ev, mInitialY)) {
mIsDragging = false;
mIsUnableToDrag = false;
// If nobody else got the focus we use it to close the layer
return super.onInterceptTouchEvent(ev);
} else {
mIsUnableToDrag = true;
}
break;
case MotionEventCompat.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 (!mEnabled || !mIsDragging && !mLastTouchAllowed && !allowSlidingFromHereX(ev, mInitialX)
&& !allowSlidingFromHereY(ev, mInitialY)) {
return false;
}
final int action = ev.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_OUTSIDE) {
mLastTouchAllowed = false;
} else {
mLastTouchAllowed = true;
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
completeScroll();
// Remember where the motion event started
mLastX = mInitialX = ev.getX();
mLastY = mInitialY = ev.getY();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
case MotionEvent.ACTION_MOVE:
if (!mIsDragging) {
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (pointerIndex == -1) {
mActivePointerId = INVALID_POINTER;
break;
}
final float x = MotionEventCompat.getX(ev, pointerIndex);
final float xDiff = Math.abs(x - mLastX);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float yDiff = Math.abs(y - mLastY);
if (xDiff > mTouchSlop && xDiff > yDiff) {
mIsDragging = true;
mLastX = x;
setDrawingCacheEnabled(true);
} else if (yDiff > mTouchSlop && yDiff > xDiff) {
mIsDragging = true;
mLastY = y;
setDrawingCacheEnabled(true);
}
}
if (mIsDragging) {
// Scroll to follow the motion event
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
if (activePointerIndex == -1) {
mActivePointerId = INVALID_POINTER;
break;
}
final float x = MotionEventCompat.getX(ev, activePointerIndex);
final float y = MotionEventCompat.getY(ev, activePointerIndex);
final float deltaX = mLastX - x;
final float deltaY = mLastY - y;
mLastX = x;
mLastY = y;
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_MIDDLE:
topBound = getHeight();
bottomBound = -getHeight();
leftBound = getWidth();
rightBound = -getWidth();
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:
topBound = rightBound = leftBound = 0;
bottomBound = -getHeight();
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;
scrollTo((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 activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
final float y = MotionEventCompat.getY(ev, activePointerIndex);
final int totalDeltaX = (int) (x - mInitialX);
final int totalDeltaY = (int) (y - mInitialY);
boolean nextStateOpened = determineNextStateOpened(mIsOpen, scrollX, scrollY, initialVelocityX,
initialVelocityY, totalDeltaX, totalDeltaY);
switchLayer(nextStateOpened, true, true, initialVelocityX, initialVelocityY);
mActivePointerId = INVALID_POINTER;
endDrag();
} else if (mIsOpen && closeOnTapEnabled) {
closeLayer(true);
} else if (!mIsOpen && openOnTapEnabled) {
openLayer(true);
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
switchLayer(mIsOpen, true, true);
mActivePointerId = INVALID_POINTER;
endDrag();
}
break;
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
mLastX = MotionEventCompat.getX(ev, index);
mLastY = MotionEventCompat.getY(ev, index);
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
mLastX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
mLastY = MotionEventCompat.getY(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
break;
}
if (mActivePointerId == INVALID_POINTER) {
mLastTouchAllowed = false;
}
return true;
}
private boolean allowSlidingFromHereX(final MotionEvent ev, final float initialX) {
switch (mScreenSide) {
case STICK_TO_LEFT:
case STICK_TO_RIGHT:
case STICK_TO_MIDDLE:
if (mIsOpen) {
return true;
}
if (!mIsOpen && mOffsetWidth > 0) {
switch (mScreenSide) {
case STICK_TO_LEFT:
return initialX <= mOffsetWidth;
case STICK_TO_RIGHT:
return initialX >= getWidth() - mOffsetWidth;
}
}
default:
return false;
}
}
private boolean allowSlidingFromHereY(final MotionEvent ev, final float initialY) {
switch (mScreenSide) {
case STICK_TO_TOP:
case STICK_TO_BOTTOM:
case STICK_TO_MIDDLE:
if (mIsOpen) {
return true;
}
if (!mIsOpen && mOffsetWidth > 0) {
switch (mScreenSide) {
case STICK_TO_TOP:
return initialY <= mOffsetWidth;
case STICK_TO_BOTTOM:
return initialY >= getHeight() - mOffsetWidth;
}
}
default:
return false;
}
}
/**
* Checks if the touch event is valid for dragging the view.
*
* @param dx
* changed in delta from the initialX
* @param initialX
* where the touch event started.
* @return true if you can drag this view, false otherwise
*/
private boolean allowDragingX(final float dx, final float initialX) {
if (mIsOpen && getLeft() <= initialX || getRight() >= initialX) {
switch (mScreenSide) {
case STICK_TO_RIGHT:
return dx > 0;
case STICK_TO_LEFT:
return dx < 0;
case STICK_TO_MIDDLE:
return dx != 0;
}
}
if (!mIsOpen && mOffsetWidth > 0 && dx > 0) {
switch (mScreenSide) {
case STICK_TO_LEFT:
return initialX <= mOffsetWidth && dx > 0;
case STICK_TO_RIGHT:
return initialX >= getWidth() - mOffsetWidth && dx < 0;
case STICK_TO_MIDDLE:
return dx != 0;
}
}
return false;
}
private boolean allowDragingY(final float dy, final float initialY) {
if (mIsOpen && getTop() <= initialY || getBottom() >= initialY) {
switch (mScreenSide) {
case STICK_TO_BOTTOM:
return mIsOpen && dy > 0;
case STICK_TO_TOP:
return mIsOpen && dy < 0;
case STICK_TO_MIDDLE:
return mIsOpen && dy != 0;
}
}
if (!mIsOpen && mOffsetWidth > 0 && dy > 0) {
switch (mScreenSide) {
case STICK_TO_TOP:
return initialY <= mOffsetWidth && dy > 0;
case STICK_TO_BOTTOM:
return initialY >= getHeight() - mOffsetWidth && dy < 0;
case STICK_TO_MIDDLE:
return dy != 0;
}
}
return false;
}
/**
* Based on the current state, position and velocity of the layer we calculate what the next state should be.
*
* @param currentState
* @param swipeOffsetX
* @param swipeOffsetY
* @param velocityX
* @param velocityY
* @param deltaX
* @param deltaY
* @return true means we should open it, false close it.
*/
private boolean determineNextStateOpened(final boolean currentState, final float swipeOffsetX,
final float swipeOffsetY, final int velocityX, final int velocityY, final int deltaX, final int deltaY) {
final boolean targetState;
final boolean calcX;
final boolean calcY;
// Work out which velocity we should listen to.
switch (mScreenSide) {
case STICK_TO_TOP:
case STICK_TO_BOTTOM:
calcY = true;
calcX = false;
break;
case STICK_TO_RIGHT:
case STICK_TO_LEFT:
calcX = true;
calcY = false;
break;
case STICK_TO_MIDDLE:
calcX = calcY = true;
break;
default:
calcX = calcY = false;
break;
}
if (calcX && Math.abs(deltaX) > mFlingDistance && Math.abs(velocityX) > mMinimumVelocity) {
targetState = mScreenSide == STICK_TO_RIGHT && velocityX <= 0 || mScreenSide == STICK_TO_LEFT
&& velocityX > 0;
} else if (calcY && Math.abs(deltaY) > mFlingDistance && Math.abs(velocityY) > mMinimumVelocity) {
targetState = mScreenSide == STICK_TO_BOTTOM && velocityY <= 0 || mScreenSide == STICK_TO_TOP
&& velocityY > 0;
} else {
final int w = getWidth();
final int h = getHeight();
switch (mScreenSide) {
case STICK_TO_RIGHT:
targetState = swipeOffsetX > -w / 2;
break;
case STICK_TO_BOTTOM:
targetState = swipeOffsetY > -h / 2;
break;
case STICK_TO_LEFT:
targetState = swipeOffsetX < w / 2;
break;
case STICK_TO_TOP:
targetState = swipeOffsetY < h / 2;
break;
case STICK_TO_MIDDLE:
targetState = Math.abs(swipeOffsetX) < w / 2 && Math.abs(swipeOffsetY) < h / 2;
break;
default:
targetState = true;
}
}
return targetState;
}
/**
* 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 (mIsOpen) {
if (mOnInteractListener != null) {
mOnInteractListener.onOpened();
}
} else {
if (mOnInteractListener != null) {
mOnInteractListener.onClosed();
}
}
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 = 0;
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);
invalidate();
}
// 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 FloatMath.sin(f);
}
private void endDrag() {
mIsDragging = false;
mIsUnableToDrag = false;
mLastTouchAllowed = 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 = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, 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 = MotionEventCompat.getX(ev, newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, 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) {
scrollTo(x, y);
}
if (mIsOpen) {
if (mOnInteractListener != null) {
mOnInteractListener.onOpened();
}
} else {
if (mOnInteractListener != null) {
mOnInteractListener.onClosed();
}
}
}
mScrolling = false;
}
/**
* 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_AUTO}, {@link #STICK_TO_BOTTOM}, {@link #STICK_TO_LEFT}, {@link #STICK_TO_MIDDLE}
* {@link #STICK_TO_RIGHT}, {@link #STICK_TO_TOP}
*/
public void setStickTo(int screenSide) {
if (screenSide != STICK_TO_AUTO) {
mForceLayout = true;
}
mScreenSide = screenSide;
closeLayer(false, true);
}
/**
* If parameter is set to <code>true</code>, whenever the <code>SlidingLayer</code> is tapped and
* the SlidingLayer is opened, it will attempt to close.
* If parameter is set to <code>false</code>, then tapping the <code>SlidingLayer</code> will
* do nothing
*
* @param _closeOnTapEnabled
*/
public void setCloseOnTapEnabled(boolean _closeOnTapEnabled) {
closeOnTapEnabled = _closeOnTapEnabled;
}
/**
* Given that there is a visible offset and it is tapped, if the parameter is set
* to true it will attempt to open the <code>SlidingLayer</code>. If parameter is false,
* tapping a visible offset will yield no result.
* @param _openOnTapEnabled
*/
public void setOpenOnTapEnabled(boolean _openOnTapEnabled) {
openOnTapEnabled = _openOnTapEnabled;
}
@SuppressWarnings("deprecation")
private int getScreenSideAuto(int newLeft, int newRight) {
int newScreenSide;
if (mScreenSide == STICK_TO_AUTO) {
int screenWidth;
Display display = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
try {
Class<?> cls = Display.class;
Class<?>[] parameterTypes = { Point.class };
Point parameter = new Point();
Method method = cls.getMethod("getSize", parameterTypes);
method.invoke(display, parameter);
screenWidth = parameter.x;
} catch (Exception e) {
screenWidth = display.getWidth();
}
boolean boundToLeftBorder = newLeft == 0;
boolean boundToRightBorder = newRight == screenWidth;
if (boundToLeftBorder == boundToRightBorder
&& getLayoutParams().width == android.view.ViewGroup.LayoutParams.MATCH_PARENT) {
newScreenSide = STICK_TO_MIDDLE;
} else if (boundToLeftBorder) {
newScreenSide = STICK_TO_LEFT;
} else {
newScreenSide = STICK_TO_RIGHT;
}
} else {
newScreenSide = mScreenSide;
}
return newScreenSide;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getDefaultSize(0, widthMeasureSpec);
int height = getDefaultSize(0, heightMeasureSpec);
setMeasuredDimension(width, height);
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);
// Make sure scroll position is set correctly.
if (w != oldw) {
completeScroll();
int[] pos = getDestScrollPos();
scrollTo(pos[0], pos[1]);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int screenSide = mScreenSide;
if (mScreenSide == STICK_TO_AUTO) {
screenSide = getScreenSideAuto(left, right);
}
if (screenSide != mScreenSide || mForceLayout) {
mForceLayout = false;
mScreenSide = screenSide;
closeLayer(false, true);
if (mScreenSide == STICK_TO_RIGHT) {
setPadding(getPaddingLeft() + mShadowWidth, getPaddingTop(), getPaddingRight(), getPaddingBottom());
} else if (mScreenSide == STICK_TO_BOTTOM) {
setPadding(getPaddingLeft(), getPaddingTop() + mShadowWidth, getPaddingRight(), getPaddingBottom());
} else if (mScreenSide == STICK_TO_LEFT) {
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight() + mShadowWidth, getPaddingBottom());
} else if (mScreenSide == STICK_TO_TOP) {
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom() + mShadowWidth);
} else if (mScreenSide == STICK_TO_MIDDLE) {
setPadding(getPaddingLeft() + mShadowWidth, getPaddingTop(), getPaddingRight() + mShadowWidth,
getPaddingBottom());
}
}
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
private int[] getDestScrollPos() {
return getDestScrollPos(0, 0);
}
/**
* Get the x destination based on the velocity
*
* @param xValue
* @param yValue
* @return
* @since 1.0
*
*/
private int[] getDestScrollPos(int xValue, int yValue) {
int[] pos = new int[2];
if (mIsOpen) {
return pos;
} else {
switch (mScreenSide) {
case STICK_TO_RIGHT:
pos[0] = -getWidth() + mOffsetWidth;
break;
case STICK_TO_LEFT:
pos[0] = getWidth() - mOffsetWidth;
break;
case STICK_TO_TOP:
pos[1] = getHeight() - mOffsetWidth;
break;
case STICK_TO_BOTTOM:
pos[1] = -getHeight() + mOffsetWidth;
break;
case STICK_TO_MIDDLE:
// Calculate slope m to get direction of swiping and apply the same vector until the end of the
// animation
float m = 1;
// If no veocity nor translation (difficult to get) the target is random
if (xValue == 0 && yValue == 0) {
m = mRandom != null ? (float) Math.tan(mRandom.nextFloat() * Math.PI - Math.PI / 2) : 1;
} else if (xValue == 0) {
// Avoid division by 0 (Get the max value of the tan which is equivalent)
m = (float) Math.tan(Math.PI / 2);
} else {
// Get slope
m = yValue / (float) xValue;
}
if (Math.abs(m) >= 1) {
pos[0] = Math.round(getOperationSignForDiffMeasure(xValue) * getHeight() / Math.abs(m)
- (mLastX - getWidth() / 2));
pos[1] = Math.round(getOperationSignForDiffMeasure(yValue) * getHeight());
} else {
pos[0] = Math.round(getOperationSignForDiffMeasure(xValue) * getWidth());
pos[1] = Math.round(getOperationSignForDiffMeasure(yValue) * getWidth() * Math.abs(m)
- (mLastY - getHeight() / 2));
}
break;
}
return pos;
}
}
private int getOperationSignForDiffMeasure(float d) {
if (mRandom == null) {
return 1;
} else {
return Math.abs(d) < mMinimumVelocity ? mRandom.nextBoolean() ? 1 : -1 : d > 0 ? -1 : 1;
}
}
public int getContentLeft() {
return getLeft() + getPaddingLeft();
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// Draw the margin drawable if needed.
if (mShadowWidth > 0 && mShadowDrawable != null) {
if (mScreenSide == STICK_TO_RIGHT) {
mShadowDrawable.setBounds(0, 0, mShadowWidth, getHeight());
}
if (mScreenSide == STICK_TO_TOP) {
mShadowDrawable.setBounds(0, getHeight() - mShadowWidth, getWidth(), getHeight());
}
if (mScreenSide == STICK_TO_LEFT) {
mShadowDrawable.setBounds(getWidth() - mShadowWidth, 0, getWidth(), getHeight());
}
if (mScreenSide == STICK_TO_BOTTOM) {
mShadowDrawable.setBounds(0, 0, getWidth(), mShadowWidth);
}
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) {
scrollTo(x, y);
}
// We invalidate a slightly larger area now, this was only optimised for right menu previously
// Keep on drawing until the animation has finished. Just re-draw the necessary part
invalidate(getLeft() + oldX, getTop() + oldY, getRight() - oldX, getBottom() - oldY);
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.
*/
public void onOpen();
/**
* 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.
*/
public void onClose();
/**
* this method is executed after <code>onOpen()</code>, when the animation has finished.
*/
public void onOpened();
/**
* this method is executed after <code>onClose()</code>, when the animation has finished and the <code>SlidingLayer</code> is
* therefore no longer visible.
*/
public void onClosed();
}
}