package net.simonvt.widget;
import net.simonvt.menudrawer.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
public abstract class MenuDrawer extends ViewGroup {
/**
* Callback interface for changing state of the drawer.
*/
public interface OnDrawerStateChangeListener {
/**
* Called when the drawer state changes.
*
* @param oldState The old drawer state.
* @param newState The new drawer state.
*/
void onDrawerStateChange(int oldState, int newState);
}
/**
* Tag used when logging.
*/
private static final String TAG = "MenuDrawer";
/**
* Indicates whether debug code should be enabled.
*/
private static final boolean DEBUG = false;
/**
* Key used when saving menu visibility state.
*/
private static final String STATE_MENU_VISIBLE = "net.simonvt.menudrawer.view.menu.menuVisible";
/**
* The time between each frame when animating the drawer.
*/
protected static final int ANIMATION_DELAY = 1000 / 60;
/**
* Interpolator used for stretching/retracting the arrow indicator.
*/
protected static final Interpolator ARROW_INTERPOLATOR = new AccelerateInterpolator();
/**
* Interpolator used for peeking at the drawer.
*/
private static final Interpolator PEEK_INTERPOLATOR = new PeekInterpolator();
/**
* Interpolator used when animating the drawer open/closed.
*/
private static final Interpolator SMOOTH_INTERPOLATOR = new SmoothInterpolator();
/**
* Default delay from {@link #peekDrawer()} is called until first animation is run.
*/
private static final long DEFAULT_PEEK_START_DELAY = 5000;
/**
* Default delay between each subsequent animation, after {@link #peekDrawer()} has been called.
*/
private static final long DEFAULT_PEEK_DELAY = 10000;
/**
* The duration of the peek animation.
*/
private static final int PEEK_DURATION = 5000;
/**
* The maximum touch area width of the drawer in dp.
*/
private static final int MAX_DRAG_BEZEL_DP = 24;
/**
* The maximum animation duration.
*/
private static final int DURATION_MAX = 600;
/**
* The maximum alpha of the dark menu overlay used for dimming the menu.
*/
protected static final int MAX_MENU_OVERLAY_ALPHA = 185;
/**
* Drag mode for sliding only the content view.
*/
public static final int MENU_DRAG_CONTENT = 0;
/**
* Drag mode for sliding the entire window.
*/
public static final int MENU_DRAG_WINDOW = 1;
/**
* Position the menu to the left of the content.
*/
public static final int MENU_POSITION_LEFT = 0;
/**
* Position the menu to the right of the content.
*/
public static final int MENU_POSITION_RIGHT = 1;
/**
* Disallow opening the drawer by dragging the screen.
*/
public static final int TOUCH_MODE_NONE = 0;
/**
* Allow opening drawer only by dragging on the edge of the screen.
*/
public static final int TOUCH_MODE_BEZEL = 1;
/**
* Allow opening drawer by dragging anywhere on the screen.
*/
public static final int TOUCH_MODE_FULLSCREEN = 2;
/**
* Indicates that the drawer is currently closed.
*/
public static final int STATE_CLOSED = 0;
/**
* Indicates that the drawer is currently closing.
*/
public static final int STATE_CLOSING = 1;
/**
* Indicates that the drawer is currently being dragged by the user.
*/
public static final int STATE_DRAGGING = 2;
/**
* Indicates that the drawer is currently opening.
*/
public static final int STATE_OPENING = 4;
/**
* Indicates that the drawer is currently open.
*/
public static final int STATE_OPEN = 8;
/**
* Distance in dp from closed position from where the drawer is considered closed with regards to touch events.
*/
private static final int CLOSE_ENOUGH = 3;
/**
* Indicates whether to use {@link View#setTranslationX(float)} when positioning views.
*/
static final boolean USE_TRANSLATIONS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
/**
* Drawable used as menu overlay.
*/
protected Drawable mMenuOverlay;
/**
* Defines whether the drop shadow is enabled.
*/
private boolean mDropShadowEnabled;
/**
* Drawable used as content drop shadow onto the menu.
*/
protected Drawable mDropShadowDrawable;
/**
* The width of the content drop shadow.
*/
protected int mDropShadowWidth;
/**
* Arrow bitmap used to indicate the active view.
*/
protected Bitmap mArrowBitmap;
/**
* The currently active view.
*/
protected View mActiveView;
/**
* Position of the active view. This is compared to View#getTag(R.id.mdActiveViewPosition) when drawing the arrow.
*/
protected int mActivePosition;
/**
* Used when reading the position of the active view.
*/
protected final Rect mActiveRect = new Rect();
/**
* The parent of the menu view.
*/
protected BuildLayerFrameLayout mMenuContainer;
/**
* The parent of the content view.
*/
protected BuildLayerFrameLayout mContentView;
/**
* The width of the menu.
*/
protected int mMenuWidth;
/**
* Indicates whether the menu width has been set explicity either via the theme or by calling
* {@link #setMenuWidth(int)}.
*/
private boolean mMenuWidthSet;
/**
* Current left position of the content.
*/
protected int mOffsetPixels;
/**
* Indicates whether the menu is currently visible.
*/
protected boolean mMenuVisible;
/**
* The drag mode of the drawer. Can be either {@link #MENU_DRAG_CONTENT} or {@link #MENU_DRAG_WINDOW}.
*/
private int mDragMode;
/**
* The current drawer state.
*
* @see #STATE_CLOSED
* @see #STATE_CLOSING
* @see #STATE_DRAGGING
* @see #STATE_OPENING
* @see #STATE_OPEN
*/
private int mDrawerState = STATE_CLOSED;
/**
* The maximum touch area width of the drawer in px.
*/
protected int mMaxTouchBezelWidth;
/**
* The touch area width of the drawer in px.
*/
protected int mTouchWidth;
/**
* Indicates whether the drawer is currently being dragged.
*/
protected boolean mIsDragging;
/**
* Slop before starting a drag.
*/
protected final int mTouchSlop;
/**
* The initial X position of a drag.
*/
protected float mInitialMotionX;
/**
* The last X position of a drag.
*/
protected float mLastMotionX = -1;
/**
* The last Y position of a drag.
*/
protected float mLastMotionY = -1;
/**
* Runnable used when animating the drawer open/closed.
*/
private final Runnable mDragRunnable = new Runnable() {
public void run() {
postAnimationInvalidate();
}
};
/**
* Runnable used when the peek animation is running.
*/
protected final Runnable mPeekRunnable = new Runnable() {
@Override
public void run() {
peekDrawerInvalidate();
}
};
/**
* Runnable used for first call to {@link #startPeek()} after {@link #peekDrawer()} has been called.
*/
private Runnable mPeekStartRunnable;
/**
* Default delay between each subsequent animation, after {@link #peekDrawer()} has been called.
*/
protected long mPeekDelay;
/**
* Scroller used when animating the drawer open/closed.
*/
private Scroller mScroller;
/**
* Scroller used for the peek drawer animation.
*/
protected Scroller mPeekScroller;
/**
* Velocity tracker used when animating the drawer open/closed after a drag.
*/
protected VelocityTracker mVelocityTracker;
/**
* Maximum velocity allowed when animating the drawer open/closed.
*/
protected int mMaxVelocity;
/**
* Listener used to dispatch state change events.
*/
private OnDrawerStateChangeListener mOnDrawerStateChangeListener;
/**
* Indicates whether the menu should be offset when dragging the drawer.
*/
protected boolean mOffsetMenu = true;
/**
* Touch mode for the Drawer.
* Possible values are {@link #TOUCH_MODE_NONE}, {@link #TOUCH_MODE_BEZEL} or {@link #TOUCH_MODE_FULLSCREEN}
* Default: {@link #TOUCH_MODE_BEZEL}
*/
protected int mTouchMode = TOUCH_MODE_BEZEL;
/**
* Distance in px from closed position from where the drawer is considered closed with regards to touch events.
*/
protected int mCloseEnough;
/**
* Indicates whether the current layer type is {@link View#LAYER_TYPE_HARDWARE}.
*/
private boolean mLayerTypeHardware;
/**
* Indicates whether to use {@link View#LAYER_TYPE_HARDWARE} when animating the drawer.
*/
private boolean mHardwareLayersEnabled = true;
public MenuDrawer(Context context) {
this(context, null);
}
public MenuDrawer(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.menuDrawerStyle);
}
public MenuDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setWillNotDraw(false);
setFocusable(false);
TypedArray a =
context.obtainStyledAttributes(attrs, R.styleable.MenuDrawer, defStyle, R.style.Widget_MenuDrawer);
final Drawable contentBackground = a.getDrawable(R.styleable.MenuDrawer_mdContentBackground);
final Drawable menuBackground = a.getDrawable(R.styleable.MenuDrawer_mdMenuBackground);
mMenuWidth = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdMenuWidth, -1);
mMenuWidthSet = mMenuWidth != -1;
final int arrowResId = a.getResourceId(R.styleable.MenuDrawer_mdArrowDrawable, 0);
if (arrowResId != 0) {
mArrowBitmap = BitmapFactory.decodeResource(getResources(), arrowResId);
}
mDropShadowEnabled = a.getBoolean(R.styleable.MenuDrawer_mdDropShadowEnabled, true);
mDropShadowDrawable = a.getDrawable(R.styleable.MenuDrawer_mdDropShadow);
if (mDropShadowDrawable == null) {
final int dropShadowColor = a.getColor(R.styleable.MenuDrawer_mdDropShadowColor, 0xFF000000);
setDropShadowColor(dropShadowColor);
}
mDropShadowWidth = a.getDimensionPixelSize(R.styleable.MenuDrawer_mdDropShadowWidth, dpToPx(6));
a.recycle();
mMenuContainer = new BuildLayerFrameLayout(context);
mMenuContainer.setId(R.id.md__menu);
mMenuContainer.setBackgroundDrawable(menuBackground);
addView(mMenuContainer);
mContentView = new NoClickThroughFrameLayout(context);
mContentView.setId(R.id.md__content);
mContentView.setBackgroundDrawable(contentBackground);
addView(mContentView);
mMenuOverlay = new ColorDrawable(0xFF000000);
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
mMaxVelocity = configuration.getScaledMaximumFlingVelocity();
mScroller = new Scroller(context, SMOOTH_INTERPOLATOR);
mPeekScroller = new Scroller(context, PEEK_INTERPOLATOR);
mMaxTouchBezelWidth = dpToPx(MAX_DRAG_BEZEL_DP);
mCloseEnough = dpToPx(CLOSE_ENOUGH);
}
private int dpToPx(int dp) {
return (int) (getResources().getDisplayMetrics().density * dp + 0.5f);
}
/**
* Toggles the menu open and close with animation.
*/
public void toggleMenu() {
toggleMenu(true);
}
/**
* Toggles the menu open and close.
*
* @param animate Whether open/close should be animated.
*/
public void toggleMenu(boolean animate) {
if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) {
closeMenu(animate);
} else if (mDrawerState == STATE_CLOSED || mDrawerState == STATE_CLOSING) {
openMenu(animate);
}
}
/**
* Animates the menu open.
*/
public void openMenu() {
openMenu(true);
}
/**
* Opens the menu.
*
* @param animate Whether open/close should be animated.
*/
public void openMenu(boolean animate) {
animateOffsetTo(mMenuWidth, 0, animate);
}
/**
* Animates the menu closed.
*/
public void closeMenu() {
closeMenu(true);
}
/**
* Closes the menu.
*
* @param animate Whether open/close should be animated.
*/
public void closeMenu(boolean animate) {
animateOffsetTo(0, 0, animate);
}
/**
* Indicates whether the menu is currently visible.
*
* @return True if the menu is open, false otherwise.
*/
public boolean isMenuVisible() {
return mMenuVisible;
}
/**
* Set the width of the menu drawer when open.
*
* @param width
*/
public void setMenuWidth(final int width) {
mMenuWidth = width;
mMenuWidthSet = true;
if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) {
setOffsetPixels(mMenuWidth);
}
requestLayout();
invalidate();
}
/**
* Set the active view. If the mdArrowDrawable attribute is set, this View will have an arrow drawn next to it.
*
* @param v The active view.
* @param position Optional position, usually used with ListView. v.setTag(R.id.mdActiveViewPosition, position)
* must be called first.
*/
public void setActiveView(View v, int position) {
mActiveView = v;
mActivePosition = position;
invalidate();
}
/**
* Enables or disables offsetting the menu when dragging the drawer.
*
* @param offsetMenu True to offset the menu, false otherwise.
*/
public void setOffsetMenuEnabled(boolean offsetMenu) {
if (offsetMenu != mOffsetMenu) {
mOffsetMenu = offsetMenu;
requestLayout();
invalidate();
}
}
/**
* Indicates whether the menu is being offset when dragging the drawer.
*
* @return True if the menu is being offset, false otherwise.
*/
public boolean getOffsetMenuEnabled() {
return mOffsetMenu;
}
/**
* Returns the state of the drawer. Can be one of {@link #STATE_CLOSED}, {@link #STATE_CLOSING},
* {@link #STATE_DRAGGING}, {@link #STATE_OPENING} or {@link #STATE_OPEN}.
*
* @return The drawers state.
*/
public int getDrawerState() {
return mDrawerState;
}
/**
* Register a callback to be invoked when the drawer state changes.
*
* @param listener The callback that will run.
*/
public void setOnDrawerStateChangeListener(OnDrawerStateChangeListener listener) {
mOnDrawerStateChangeListener = listener;
}
/**
* Defines whether the drop shadow is enabled.
*
* @param enabled Whether the drop shadow is enabled.
*/
public void setDropShadowEnabled(boolean enabled) {
mDropShadowEnabled = enabled;
invalidate();
}
/**
* Sets the color of the drop shadow.
*
* @param color The color of the drop shadow.
*/
public abstract void setDropShadowColor(int color);
/**
* Sets the drawable of the drop shadow.
*
* @param drawable The drawable of the drop shadow.
*/
public void setDropShadow(Drawable drawable) {
mDropShadowDrawable = drawable;
invalidate();
}
/**
* Sets the drawable of the drop shadow.
*
* @param resId The resource identifier of the the drawable.
*/
public void setDropShadow(int resId) {
setDropShadow(getResources().getDrawable(resId));
}
/**
* Returns the drawable of the drop shadow.
*/
public Drawable getDropShadow() {
return mDropShadowDrawable;
}
/**
* Sets the width of the drop shadow.
*
* @param width The width of the drop shadow in px.
*/
public void setDropShadowWidth(int width) {
mDropShadowWidth = width;
invalidate();
}
/**
* Animates the drawer slightly open until the user opens the drawer.
*/
public void peekDrawer() {
peekDrawer(DEFAULT_PEEK_START_DELAY, DEFAULT_PEEK_DELAY);
}
/**
* Animates the drawer slightly open. If delay is larger than 0, this happens until the user opens the drawer.
*
* @param delay The delay (in milliseconds) between each run of the animation. If 0, this animation is only run
* once.
*/
public void peekDrawer(long delay) {
peekDrawer(DEFAULT_PEEK_START_DELAY, delay);
}
/**
* Animates the drawer slightly open. If delay is larger than 0, this happens until the user opens the drawer.
*
* @param startDelay The delay (in milliseconds) until the animation is first run.
* @param delay The delay (in milliseconds) between each run of the animation. If 0, this animation is only run
* once.
*/
public void peekDrawer(final long startDelay, final long delay) {
if (startDelay < 0) {
throw new IllegalArgumentException("startDelay must be zero or larger.");
}
if (delay < 0) {
throw new IllegalArgumentException("delay must be zero or larger");
}
removeCallbacks(mPeekRunnable);
removeCallbacks(mPeekStartRunnable);
mPeekDelay = delay;
mPeekStartRunnable = new Runnable() {
@Override
public void run() {
startPeek();
}
};
postDelayed(mPeekStartRunnable, startDelay);
}
/**
* Enables or disables the user of {@link View#LAYER_TYPE_HARDWARE} when animations views.
*
* @param enabled Whether hardware layers are enabled.
*/
public void setHardwareLayerEnabled(boolean enabled) {
if (enabled != mHardwareLayersEnabled) {
mHardwareLayersEnabled = enabled;
mMenuContainer.setHardwareLayersEnabled(enabled);
mContentView.setHardwareLayersEnabled(enabled);
stopLayerTranslation();
}
}
/**
* Sets the drawer state.
*
* @param state The drawer state. Must be one of {@link #STATE_CLOSED}, {@link #STATE_CLOSING},
* {@link #STATE_DRAGGING}, {@link #STATE_OPENING} or {@link #STATE_OPEN}.
*/
protected void setDrawerState(int state) {
if (state != mDrawerState) {
final int oldState = mDrawerState;
mDrawerState = state;
if (mOnDrawerStateChangeListener != null) mOnDrawerStateChangeListener.onDrawerStateChange(oldState, state);
if (DEBUG) logDrawerState(state);
}
}
private void logDrawerState(int state) {
switch (state) {
case STATE_CLOSED:
Log.d(TAG, "[DrawerState] STATE_CLOSED");
break;
case STATE_CLOSING:
Log.d(TAG, "[DrawerState] STATE_CLOSING");
break;
case STATE_DRAGGING:
Log.d(TAG, "[DrawerState] STATE_DRAGGING");
break;
case STATE_OPENING:
Log.d(TAG, "[DrawerState] STATE_OPENING");
break;
case STATE_OPEN:
Log.d(TAG, "[DrawerState] STATE_OPEN");
break;
default:
Log.d(TAG, "[DrawerState] Unknown: " + state);
}
}
/**
* Sets the drawer drag mode. Can be either {@link #MENU_DRAG_CONTENT} or {@link #MENU_DRAG_WINDOW}.
*
* @param dragMode The drag mode.
*/
public void setDragMode(int dragMode) {
mDragMode = dragMode;
}
/**
* Returns the touch mode.
*/
public int getTouchMode() {
return mTouchMode;
}
/**
* Sets the drawer touch mode. Possible values are {@link #TOUCH_MODE_NONE}, {@link #TOUCH_MODE_BEZEL} or
* {@link #TOUCH_MODE_FULLSCREEN}.
*
* @param dragMode The drag mode.
*/
public void setTouchMode(int mode) {
if (mTouchMode != mode) {
mTouchMode = mode;
updateTouchAreaWidth();
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
final int offsetPixels = mOffsetPixels;
drawMenuOverlay(canvas, offsetPixels);
if (mDropShadowEnabled) drawDropShadow(canvas, offsetPixels);
if (mArrowBitmap != null) drawArrow(canvas, offsetPixels);
}
/**
* Called when the content drop shadow should be drawn.
*
* @param canvas The canvas on which to draw.
* @param offsetPixels Value in pixels indicating the offset.
*/
protected abstract void drawDropShadow(Canvas canvas, int offsetPixels);
/**
* Called when the menu overlay should be drawn.
*
* @param canvas The canvas on which to draw.
* @param offsetPixels Value in pixels indicating the offset.
*/
protected abstract void drawMenuOverlay(Canvas canvas, int offsetPixels);
/**
* Called when the arrow indicator should be drawn.
*
* @param canvas The canvas on which to draw.
* @param offsetPixels Value in pixels indicating the offset.
*/
protected abstract void drawArrow(Canvas canvas, int offsetPixels);
/**
* Sets the number of pixels the content should be offset.
*
* @param offsetPixels The number of pixels to offset the content by.
*/
protected void setOffsetPixels(int offsetPixels) {
if (offsetPixels != mOffsetPixels) {
onOffsetPixelsChanged(offsetPixels);
mOffsetPixels = offsetPixels;
mMenuVisible = offsetPixels != 0;
}
}
/**
* Called when the number of pixels the content should be offset by has changed.
*
* @param offsetPixels The number of pixels to offset the content by.
*/
protected abstract void onOffsetPixelsChanged(int offsetPixels);
/**
* If possible, set the layer type to {@link View#LAYER_TYPE_HARDWARE}.
*/
protected void startLayerTranslation() {
if (USE_TRANSLATIONS && mHardwareLayersEnabled && !mLayerTypeHardware) {
mLayerTypeHardware = true;
mContentView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mMenuContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}
/**
* If the current layer type is {@link View#LAYER_TYPE_HARDWARE}, this will set it to @link View#LAYER_TYPE_NONE}.
*/
private void stopLayerTranslation() {
if (mLayerTypeHardware) {
mLayerTypeHardware = false;
mContentView.setLayerType(View.LAYER_TYPE_NONE, null);
mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Must measure with an exact size");
}
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int height = MeasureSpec.getSize(heightMeasureSpec);
if (!mMenuWidthSet) mMenuWidth = (int) (width * 0.8f);
if (mOffsetPixels == -1) setOffsetPixels(mMenuWidth);
final int menuWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, mMenuWidth);
final int menuHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
mMenuContainer.measure(menuWidthMeasureSpec, menuHeightMeasureSpec);
final int contentWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, width);
final int contentHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, height);
mContentView.measure(contentWidthMeasureSpec, contentHeightMeasureSpec);
setMeasuredDimension(width, height);
updateTouchAreaWidth();
}
@Override
protected boolean fitSystemWindows(Rect insets) {
if (mDragMode == MENU_DRAG_WINDOW) {
mMenuContainer.setPadding(0, insets.top, 0, 0);
}
return super.fitSystemWindows(insets);
}
/**
* Compute the touch area based on the touched mode.
*
* @param dragMode The drag mode.
*/
private void updateTouchAreaWidth() {
if (mTouchMode == TOUCH_MODE_BEZEL) {
mTouchWidth = Math.min(getMeasuredWidth() / 10, mMaxTouchBezelWidth);
} else if (mTouchMode == TOUCH_MODE_FULLSCREEN) {
mTouchWidth = getMeasuredWidth();
} else {
mTouchWidth = 0;
}
}
/**
* Called when a drag has been ended.
*/
private void endDrag() {
mIsDragging = false;
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/**
* Stops ongoing animation of the drawer.
*/
protected void stopAnimation() {
removeCallbacks(mDragRunnable);
mScroller.abortAnimation();
stopLayerTranslation();
}
/**
* Called when a drawer animation has successfully completed.
*/
private void completeAnimation() {
mScroller.abortAnimation();
final int finalX = mScroller.getFinalX();
setOffsetPixels(finalX);
setDrawerState(finalX == 0 ? STATE_CLOSED : STATE_OPEN);
stopLayerTranslation();
}
/**
* Moves the drawer to the position passed.
*
* @param position The position the content is moved to.
* @param velocity Optional velocity if called by releasing a drag event.
* @param animate Whether the move is animated.
*/
protected void animateOffsetTo(int position, int velocity, boolean animate) {
endDrag();
endPeek();
final int startX = mOffsetPixels;
final int dx = position - startX;
if (dx == 0 || !animate) {
setOffsetPixels(position);
setDrawerState(position == 0 ? STATE_CLOSED : STATE_OPEN);
stopLayerTranslation();
return;
}
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000.f * Math.abs((float) dx / velocity));
} else {
duration = (int) (600.f * Math.abs((float) dx / mMenuWidth));
}
duration = Math.min(duration, DURATION_MAX);
if (dx > 0) {
setDrawerState(STATE_OPENING);
mScroller.startScroll(startX, 0, dx, 0, duration);
} else {
setDrawerState(STATE_CLOSING);
mScroller.startScroll(startX, 0, dx, 0, duration);
}
startLayerTranslation();
postAnimationInvalidate();
}
/**
* Callback when each frame in the drawer animation should be drawn.
*/
private void postAnimationInvalidate() {
if (mScroller.computeScrollOffset()) {
final int oldX = mOffsetPixels;
final int x = mScroller.getCurrX();
if (x != oldX) setOffsetPixels(x);
if (x != mScroller.getFinalX()) {
postOnAnimation(mDragRunnable);
return;
}
}
completeAnimation();
}
/**
* Starts peek drawer animation.
*/
protected void startPeek() {
final int menuWidth = mMenuWidth;
final int dx = menuWidth / 3;
mPeekScroller.startScroll(0, 0, dx, 0, PEEK_DURATION);
startLayerTranslation();
peekDrawerInvalidate();
}
/**
* Callback when each frame in the peek drawer animation should be drawn.
*/
private void peekDrawerInvalidate() {
if (mPeekScroller.computeScrollOffset()) {
final int oldX = mOffsetPixels;
final int x = mPeekScroller.getCurrX();
if (x != oldX) setOffsetPixels(x);
if (!mPeekScroller.isFinished()) {
postOnAnimation(mPeekRunnable);
return;
} else if (mPeekDelay > 0) {
mPeekStartRunnable = new Runnable() {
@Override
public void run() {
startPeek();
}
};
postDelayed(mPeekStartRunnable, mPeekDelay);
}
}
completePeek();
}
/**
* Called when the peek drawer animation has successfully completed.
*/
private void completePeek() {
mPeekScroller.abortAnimation();
setOffsetPixels(0);
setDrawerState(STATE_CLOSED);
stopLayerTranslation();
}
/**
* Stops ongoing peek drawer animation.
*/
protected void endPeek() {
removeCallbacks(mPeekStartRunnable);
removeCallbacks(mPeekRunnable);
stopLayerTranslation();
}
@Override
public void postOnAnimation(Runnable action) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
super.postOnAnimation(action);
} else {
postDelayed(action, ANIMATION_DELAY);
}
}
protected boolean isCloseEnough() {
return mOffsetPixels <= mCloseEnough;
}
/**
* Returns true if the touch event occurs over the content.
*
* @param ev The motion event.
* @return True if the touch event occured over the content, false otherwise.
*/
protected abstract boolean isContentTouch(MotionEvent ev);
/**
* Returns true if dragging the content should be allowed.
*
* @param ev The motion event.
* @return True if dragging the content should be allowed, false otherwise.
*/
protected abstract boolean onDownAllowDrag(MotionEvent ev);
/**
* Returns true if dragging the content should be allowed.
*
* @param ev The motion event.
* @return True if dragging the content should be allowed, false otherwise.
*/
protected abstract boolean onMoveAllowDrag(MotionEvent ev, float dx);
/**
* Called when a move event has happened while dragging the content is in progress.
*
* @param dx The X difference between the last motion event and the current motion event.
*/
protected abstract void onMoveEvent(float dx);
/**
* Called when {@link MotionEvent#ACTION_UP} of {@link MotionEvent#ACTION_CANCEL} is delivered to
* {@link MenuDrawer#onTouchEvent(android.view.MotionEvent)}.
*
* @param ev The motion event.
*/
protected abstract void onUpEvent(MotionEvent ev);
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (action == MotionEvent.ACTION_DOWN && mMenuVisible && isCloseEnough()) {
setOffsetPixels(0);
stopAnimation();
endPeek();
setDrawerState(STATE_CLOSED);
}
// Always intercept events over the content while menu is visible.
if (mMenuVisible && isContentTouch(ev)) return true;
if (mTouchMode == TOUCH_MODE_NONE) {
return false;
}
if (action != MotionEvent.ACTION_DOWN) {
if (mIsDragging) return true;
}
switch (action) {
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = ev.getY();
final boolean allowDrag = onDownAllowDrag(ev);
if (allowDrag) {
setDrawerState(mMenuVisible ? STATE_OPEN : STATE_CLOSED);
stopAnimation();
endPeek();
mIsDragging = false;
}
break;
}
case MotionEvent.ACTION_MOVE: {
final float x = ev.getX();
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = ev.getY();
final float yDiff = Math.abs(y - mLastMotionY);
if (xDiff > mTouchSlop && xDiff > yDiff) {
final boolean allowDrag = onMoveAllowDrag(ev, dx);
if (allowDrag) {
setDrawerState(STATE_DRAGGING);
mIsDragging = true;
mLastMotionX = x;
mLastMotionY = y;
}
}
break;
}
/**
* If you click really fast, an up or cancel event is delivered here.
* Just snap content to whatever is closest.
* */
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
final int offsetPixels = mOffsetPixels;
animateOffsetTo(offsetPixels > mMenuWidth / 2 ? mMenuWidth : 0, 0, true);
break;
}
}
if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
return mIsDragging;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mMenuVisible && (mTouchMode == TOUCH_MODE_NONE)) {
return false;
}
final int action = ev.getAction() & MotionEvent.ACTION_MASK;
if (mVelocityTracker == null) mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
mLastMotionX = mInitialMotionX = ev.getX();
mLastMotionY = ev.getY();
final boolean allowDrag = onDownAllowDrag(ev);
if (allowDrag) {
stopAnimation();
endPeek();
startLayerTranslation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (!mIsDragging) {
final float x = ev.getX();
final float dx = x - mLastMotionX;
final float xDiff = Math.abs(dx);
final float y = ev.getY();
final float yDiff = Math.abs(y - mLastMotionY);
if (xDiff > mTouchSlop && xDiff > yDiff) {
final boolean allowDrag = onMoveAllowDrag(ev, dx);
if (allowDrag) {
setDrawerState(STATE_DRAGGING);
mIsDragging = true;
mLastMotionX = x - mInitialMotionX > 0
? mInitialMotionX + mTouchSlop
: mInitialMotionX - mTouchSlop;
}
}
}
if (mIsDragging) {
startLayerTranslation();
final float x = ev.getX();
final float dx = x - mLastMotionX;
mLastMotionX = x;
onMoveEvent(dx);
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
onUpEvent(ev);
break;
}
}
return true;
}
/**
* Saves the state of the drawer.
*
* @return Returns a Parcelable containing the drawer state.
*/
public Parcelable saveState() {
Bundle state = new Bundle();
final boolean menuVisible = mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING;
state.putBoolean(STATE_MENU_VISIBLE, menuVisible);
return state;
}
/**
* Restores the state of the drawer.
*
* @param in A parcelable containing the drawer state.
*/
public void restoreState(Parcelable in) {
Bundle state = (Bundle) in;
final boolean menuOpen = state.getBoolean(STATE_MENU_VISIBLE);
setOffsetPixels(menuOpen ? mMenuWidth : 0);
mDrawerState = menuOpen ? STATE_OPEN : STATE_CLOSED;
}
}