package com.ijoomer.menubuilder; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.graphics.Canvas; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; 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 DraggableDrawer extends MenuDrawer { /** * Key used when saving menu visibility state. */ private static final String STATE_MENU_VISIBLE = "com.ijoomer.ijoomermenubuilder.MenuDrawer.menuVisible"; /** * Interpolator used for stretching/retracting the active indicator. */ protected static final Interpolator INDICATOR_INTERPOLATOR = new AccelerateInterpolator(); /** * Interpolator used for peeking at the drawer. */ private static final Interpolator PEEK_INTERPOLATOR = new PeekInterpolator(); /** * The maximum alpha of the dark menu overlay used for dimming the menu. */ protected static final int MAX_MENU_OVERLAY_ALPHA = 185; /** * 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. */ protected static final int PEEK_DURATION = 5000; /** * 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; /** * Slop before starting a drag. */ protected int mTouchSlop; /** * Runnable used when the peek animation is running. */ protected final Runnable mPeekRunnable = new Runnable() { @Override public void run() { peekDrawerInvalidate(); } }; /** * Runnable used when animating the drawer open/closed. */ private final Runnable mDragRunnable = new Runnable() { @Override public void run() { postAnimationInvalidate(); } }; /** * Current left position of the content. */ protected float mOffsetPixels; /** * Indicates whether the drawer is currently being dragged. */ protected boolean mIsDragging; /** * The initial X position of a drag. */ protected float mInitialMotionX; /** * The initial Y position of a drag. */ protected float mInitialMotionY; /** * The last X position of a drag. */ protected float mLastMotionX = -1; /** * The last Y position of a drag. */ protected float mLastMotionY = -1; /** * Default delay between each subsequent animation, after {@link #peekDrawer()} has been called. */ protected long mPeekDelay; /** * 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; /** * Indicates whether the menu should be offset when dragging the drawer. */ protected boolean mOffsetMenu = true; /** * Distance in px from closed position from where the drawer is considered closed with regards to touch events. */ protected int mCloseEnough; /** * Runnable used for first call to {@link #startPeek()} after {@link #peekDrawer()} has been called. */ private Runnable mPeekStartRunnable; /** * Scroller used when animating the drawer open/closed. */ private Scroller mScroller; /** * Indicates whether the current layer type is {@link android.view.View#LAYER_TYPE_HARDWARE}. */ private boolean mLayerTypeHardware; DraggableDrawer(Activity activity, int dragMode) { super(activity, dragMode); } public DraggableDrawer(Context context) { super(context); } public DraggableDrawer(Context context, AttributeSet attrs) { super(context, attrs); } public DraggableDrawer(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void initDrawer(Context context, AttributeSet attrs, int defStyle) { super.initDrawer(context, attrs, defStyle); final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); mMaxVelocity = configuration.getScaledMaximumFlingVelocity(); mScroller = new Scroller(context, MenuDrawer.SMOOTH_INTERPOLATOR); mPeekScroller = new Scroller(context, DraggableDrawer.PEEK_INTERPOLATOR); mCloseEnough = dpToPx(DraggableDrawer.CLOSE_ENOUGH); } public void toggleMenu(boolean animate) { if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) { closeMenu(animate); } else if (mDrawerState == STATE_CLOSED || mDrawerState == STATE_CLOSING) { openMenu(animate); } } public boolean isMenuVisible() { return mMenuVisible; } public void setMenuSize(final int size) { mMenuSize = size; mMenuSizeSet = true; if (mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING) { setOffsetPixels(mMenuSize); } requestLayout(); invalidate(); } public void setOffsetMenuEnabled(boolean offsetMenu) { if (offsetMenu != mOffsetMenu) { mOffsetMenu = offsetMenu; requestLayout(); invalidate(); } } public boolean getOffsetMenuEnabled() { return mOffsetMenu; } public void peekDrawer() { peekDrawer(DEFAULT_PEEK_START_DELAY, DEFAULT_PEEK_DELAY); } public void peekDrawer(long delay) { peekDrawer(DEFAULT_PEEK_START_DELAY, delay); } 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); } public void setHardwareLayerEnabled(boolean enabled) { if (enabled != mHardwareLayersEnabled) { mHardwareLayersEnabled = enabled; mMenuContainer.setHardwareLayersEnabled(enabled); mContentContainer.setHardwareLayersEnabled(enabled); stopLayerTranslation(); } } public int getTouchMode() { return mTouchMode; } public void setTouchMode(int mode) { if (mTouchMode != mode) { mTouchMode = mode; updateTouchAreaSize(); } } public void setTouchBezelSize(int size) { mTouchBezelSize = size; } public int getTouchBezelSize() { return mTouchBezelSize; } /** * Sets the number of pixels the content should be offset. * * @param offsetPixels The number of pixels to offset the content by. */ protected void setOffsetPixels(float offsetPixels) { final int oldOffset = (int) mOffsetPixels; final int newOffset = (int) offsetPixels; mOffsetPixels = offsetPixels; if (newOffset != oldOffset) { onOffsetPixelsChanged(newOffset); mMenuVisible = newOffset != 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 android.view.View#LAYER_TYPE_HARDWARE}. */ protected void startLayerTranslation() { if (USE_TRANSLATIONS && mHardwareLayersEnabled && !mLayerTypeHardware) { mLayerTypeHardware = true; mContentContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); mMenuContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null); } } /** * If the current layer type is {@link android.view.View#LAYER_TYPE_HARDWARE}, this will set it to * {@link View#LAYER_TYPE_NONE}. */ private void stopLayerTranslation() { if (mLayerTypeHardware) { mLayerTypeHardware = false; mContentContainer.setLayerType(View.LAYER_TYPE_NONE, null); mMenuContainer.setLayerType(View.LAYER_TYPE_NONE, null); } } /** * Compute the touch area based on the touch mode. */ protected void updateTouchAreaSize() { if (mTouchMode == TOUCH_MODE_BEZEL) { mTouchSize = mTouchBezelSize; } else if (mTouchMode == TOUCH_MODE_FULLSCREEN) { mTouchSize = getMeasuredWidth(); } else { mTouchSize = 0; } } /** * Called when a drag has been ended. */ protected 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 = (int) 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 / mMenuSize)); } duration = Math.min(duration, mMaxAnimationDuration); 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. */ @SuppressLint("NewApi") private void postAnimationInvalidate() { if (mScroller.computeScrollOffset()) { final int oldX = (int) 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() { initPeekScroller(); startLayerTranslation(); peekDrawerInvalidate(); } protected abstract void initPeekScroller(); /** * Callback when each frame in the peek drawer animation should be drawn. */ private void peekDrawerInvalidate() { if (mPeekScroller.computeScrollOffset()) { final int oldX = (int) 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(); } protected boolean isCloseEnough() { return Math.abs(mOffsetPixels) <= mCloseEnough; } /** * Returns true if the touch event occurs over the content. * * @param ev The motion event. * @return True if the touch event occurred 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); /** * Tests scrollability within child views of v given a delta of dx. * * @param v View to test for horizontal scrollability * @param checkV Whether the view should be checked for draggability * @param dx Delta scrolled in pixels * @param x X coordinate of the active touch point * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ protected boolean canChildScrollHorizontally(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { final View child = group.getChildAt(i); final int childLeft = child.getLeft() + supportGetTranslationX(child); final int childRight = child.getRight() + supportGetTranslationX(child); final int childTop = child.getTop() + supportGetTranslationY(child); final int childBottom = child.getBottom() + supportGetTranslationY(child); if (x >= childLeft && x < childRight && y >= childTop && y < childBottom && canChildScrollHorizontally(child, true, dx, x - childLeft, y - childTop)) { return true; } } } return checkV && mOnInterceptMoveEventListener.isViewDraggable(v, dx, x, y); } /** * Tests scrollability within child views of v given a delta of dx. * * @param v View to test for horizontal scrollability * @param checkV Whether the view should be checked for draggability * @param dx Delta scrolled in pixels * @param x X coordinate of the active touch point * @param y Y coordinate of the active touch point * @return true if child views of v can be scrolled by delta of dx. */ protected boolean canChildScrollVertically(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { final View child = group.getChildAt(i); final int childLeft = child.getLeft() + supportGetTranslationX(child); final int childRight = child.getRight() + supportGetTranslationX(child); final int childTop = child.getTop() + supportGetTranslationY(child); final int childBottom = child.getBottom() + supportGetTranslationY(child); if (x >= childLeft && x < childRight && y >= childTop && y < childBottom && canChildScrollVertically(child, true, dx, x - childLeft, y - childTop)) { return true; } } } return checkV && mOnInterceptMoveEventListener.isViewDraggable(v, dx, x, y); } private int supportGetTranslationY(View v) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { return (int) v.getTranslationY(); } return 0; } private int supportGetTranslationX(View v) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { return (int) v.getTranslationX(); } return 0; } /** * 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 android.view.MotionEvent#ACTION_UP} of {@link android.view.MotionEvent#ACTION_CANCEL} is * delivered to {@link com.ijoomer.menubuilder.MenuDrawer#onTouchEvent(android.view.MotionEvent)}. * * @param ev The motion event. */ protected abstract void onUpEvent(MotionEvent ev); @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); final int offsetPixels = (int) mOffsetPixels; if (offsetPixels != 0) drawMenuOverlay(canvas, offsetPixels); if (mDropShadowEnabled) drawDropShadow(canvas, offsetPixels); if (mActiveIndicator != null) drawIndicator(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 active indicator should be drawn. * * @param canvas The canvas on which to draw. * @param offsetPixels Value in pixels indicating the offset. */ protected abstract void drawIndicator(Canvas canvas, int offsetPixels); void saveState(Bundle state) { final boolean menuVisible = mDrawerState == STATE_OPEN || mDrawerState == STATE_OPENING; state.putBoolean(STATE_MENU_VISIBLE, menuVisible); } public void restoreState(Parcelable in) { super.restoreState(in); Bundle state = (Bundle) in; final boolean menuOpen = state.getBoolean(STATE_MENU_VISIBLE); if (menuOpen) { openMenu(false); } else { setOffsetPixels(0); } mDrawerState = menuOpen ? STATE_OPEN : STATE_CLOSED; } }