/* * Modified so that setmodelocked does not prevent back to close drawer * * * Copyright (C) 2013 The Android Open Source Project * * 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.abewy.android.apps.klyph.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.GravityCompat; import android.support.v4.view.KeyEventCompat; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewGroupCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.widget.DrawerLayout; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; /** * DrawerLayout acts as a top-level container for window content that allows for * interactive "drawer" views to be pulled out from the edge of the window. * * <p>Drawer positioning and layout is controlled using the <code>android:layout_gravity</code> * attribute on child views corresponding to which side of the view you want the drawer * to emerge from: left or right. (Or start/end on platform versions that support layout direction.) * </p> * * <p>To use a DrawerLayout, position your primary content view as the first child with * a width and height of <code>match_parent</code>. Add drawers as child views after the main * content view and set the <code>layout_gravity</code> appropriately. Drawers commonly use * <code>match_parent</code> for height with a fixed width.</p> * * <p>{@link DrawerListener} can be used to monitor the state and motion of drawer views. * Avoid performing expensive operations such as layout during animation as it can cause * stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state. * {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.</p> * * <p>As per the Android Design guide, any drawers positioned to the left/start should * always contain content for navigating around the application, whereas any drawers * positioned to the right/end should always contain actions to take on the current content. * This preserves the same navigation left, actions right structure present in the Action Bar * and elsewhere.</p> */ public class KlyphDrawerLayout extends ViewGroup { private static final String TAG = "KlyphDrawerLayout"; /** * Indicates that any drawers are in an idle, settled state. No animation is in progress. */ public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; /** * Indicates that a drawer is currently being dragged by the user. */ public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; /** * Indicates that a drawer is in the process of settling to a final position. */ public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; /** * The drawer is unlocked. */ public static final int LOCK_MODE_UNLOCKED = 0; /** * The drawer is locked closed. The user may not open it, though * the app may open it programmatically. */ public static final int LOCK_MODE_LOCKED_CLOSED = 1; /** * The drawer is locked open. The user may not close it, though the app * may close it programmatically. */ public static final int LOCK_MODE_LOCKED_OPEN = 2; private static final int MIN_DRAWER_MARGIN = 64; // dp private static final int DEFAULT_SCRIM_COLOR = 0x99000000; /** * Length of time to delay before peeking the drawer. */ private static final int PEEK_DELAY = 160; // ms /** * Minimum velocity that will be detected as a fling */ private static final int MIN_FLING_VELOCITY = 400; // dips per second /** * Experimental feature. */ private static final boolean ALLOW_EDGE_LOCK = false; private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_gravity }; private int mMinDrawerMargin; private int mScrimColor = DEFAULT_SCRIM_COLOR; private float mScrimOpacity; private Paint mScrimPaint = new Paint(); private final ViewDragHelper mLeftDragger; private final ViewDragHelper mRightDragger; private final ViewDragCallback mLeftCallback; private final ViewDragCallback mRightCallback; private int mDrawerState; private boolean mInLayout; private boolean mFirstLayout = true; private int mLockModeLeft; private int mLockModeRight; private boolean mDisallowInterceptRequested; private boolean mChildrenCanceledTouch; private android.support.v4.widget.DrawerLayout.DrawerListener mListener; private float mInitialMotionX; private float mInitialMotionY; private Drawable mShadowLeft; private Drawable mShadowRight; /** * Listener for monitoring events about drawers. */ public interface DrawerListener { /** * Called when a drawer's position changes. * @param drawerView The child view that was moved * @param slideOffset The new offset of this drawer within its range, from 0-1 */ public void onDrawerSlide(View drawerView, float slideOffset); /** * Called when a drawer has settled in a completely open state. * The drawer is interactive at this point. * * @param drawerView Drawer view that is now open */ public void onDrawerOpened(View drawerView); /** * Called when a drawer has settled in a completely closed state. * * @param drawerView Drawer view that is now closed */ public void onDrawerClosed(View drawerView); /** * Called when the drawer motion state changes. The new state will * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. * * @param newState The new drawer motion state */ public void onDrawerStateChanged(int newState); } /** * Stub/no-op implementations of all methods of {@link DrawerListener}. * Override this if you only care about a few of the available callback methods. */ public static abstract class SimpleDrawerListener implements DrawerListener { @Override public void onDrawerSlide(View drawerView, float slideOffset) { } @Override public void onDrawerOpened(View drawerView) { } @Override public void onDrawerClosed(View drawerView) { } @Override public void onDrawerStateChanged(int newState) { } } public KlyphDrawerLayout(Context context) { this(context, null); } public KlyphDrawerLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public KlyphDrawerLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final float density = getResources().getDisplayMetrics().density; mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); final float minVel = MIN_FLING_VELOCITY * density; mLeftCallback = new ViewDragCallback(Gravity.LEFT); mRightCallback = new ViewDragCallback(Gravity.RIGHT); mLeftDragger = ViewDragHelper.create(this, 0.5f, mLeftCallback); mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); mLeftDragger.setMinVelocity(minVel); mLeftCallback.setDragger(mLeftDragger); mRightDragger = ViewDragHelper.create(this, 0.5f, mRightCallback); mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); mRightDragger.setMinVelocity(minVel); mRightCallback.setDragger(mRightDragger); // So that we can catch the back button setFocusableInTouchMode(true); ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); ViewGroupCompat.setMotionEventSplittingEnabled(this, false); } /** * Set a simple drawable used for the left or right shadow. * The drawable provided must have a nonzero intrinsic width. * * @param shadowDrawable Shadow drawable to use at the edge of a drawer * @param gravity Which drawer the shadow should apply to */ public void setDrawerShadow(Drawable shadowDrawable, int gravity) { /* * TODO Someone someday might want to set more complex drawables here. * They're probably nuts, but we might want to consider registering callbacks, * setting states, etc. properly. */ final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { mShadowLeft = shadowDrawable; invalidate(); } if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { mShadowRight = shadowDrawable; invalidate(); } } /** * Set a simple drawable used for the left or right shadow. * The drawable provided must have a nonzero intrinsic width. * * @param resId Resource id of a shadow drawable to use at the edge of a drawer * @param gravity Which drawer the shadow should apply to */ public void setDrawerShadow(int resId, int gravity) { setDrawerShadow(getResources().getDrawable(resId), gravity); } /** * Set a color to use for the scrim that obscures primary content while a drawer is open. * * @param color Color to use in 0xAARRGGBB format. */ public void setScrimColor(int color) { mScrimColor = color; invalidate(); } /** * Set a listener to be notified of drawer events. * * @param listener Listener to notify when drawer events occur * @see DrawerListener */ public void setDrawerListener(android.support.v4.widget.DrawerLayout.DrawerListener listener) { mListener = listener; } /** * Enable or disable interaction with all drawers. * * <p>This allows the application to restrict the user's ability to open or close * any drawer within this layout. DrawerLayout will still respond to calls to * {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.</p> * * <p>Locking drawers open or closed will implicitly open or close * any drawers as appropriate.</p> * * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. */ public void setDrawerLockMode(int lockMode) { setDrawerLockMode(lockMode, Gravity.LEFT); setDrawerLockMode(lockMode, Gravity.RIGHT); } /** * Enable or disable interaction with the given drawer. * * <p>This allows the application to restrict the user's ability to open or close * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, * {@link #closeDrawer(int)} and friends if a drawer is locked.</p> * * <p>Locking a drawer open or closed will implicitly open or close * that drawer as appropriate.</p> * * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. * @param edgeGravity Gravity.LEFT, RIGHT, START or END. * Expresses which drawer to change the mode for. * * @see #LOCK_MODE_UNLOCKED * @see #LOCK_MODE_LOCKED_CLOSED * @see #LOCK_MODE_LOCKED_OPEN */ public void setDrawerLockMode(int lockMode, int edgeGravity) { final int absGrav = GravityCompat.getAbsoluteGravity(edgeGravity, ViewCompat.getLayoutDirection(this)); if (absGrav == Gravity.LEFT) { mLockModeLeft = lockMode; } else if (absGrav == Gravity.RIGHT) { mLockModeRight = lockMode; } if (lockMode != LOCK_MODE_UNLOCKED) { // Cancel interaction in progress final ViewDragHelper helper = absGrav == Gravity.LEFT ? mLeftDragger : mRightDragger; helper.cancel(); } switch (lockMode) { case LOCK_MODE_LOCKED_OPEN: final View toOpen = findDrawerWithGravity(absGrav); if (toOpen != null) { openDrawer(toOpen); } break; case LOCK_MODE_LOCKED_CLOSED: final View toClose = findDrawerWithGravity(absGrav); if (toClose != null) { closeDrawer(toClose); } break; // default: do nothing } } /** * Enable or disable interaction with the given drawer. * * <p>This allows the application to restrict the user's ability to open or close * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, * {@link #closeDrawer(int)} and friends if a drawer is locked.</p> * * <p>Locking a drawer open or closed will implicitly open or close * that drawer as appropriate.</p> * * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. * @param drawerView The drawer view to change the lock mode for * * @see #LOCK_MODE_UNLOCKED * @see #LOCK_MODE_LOCKED_CLOSED * @see #LOCK_MODE_LOCKED_OPEN */ public void setDrawerLockMode(int lockMode, View drawerView) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a " + "drawer with appropriate layout_gravity"); } setDrawerLockMode(lockMode, getDrawerViewGravity(drawerView)); } /** * Check the lock mode of the drawer with the given gravity. * * @param edgeGravity Gravity of the drawer to check * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or * {@link #LOCK_MODE_LOCKED_OPEN}. */ public int getDrawerLockMode(int edgeGravity) { final int absGrav = GravityCompat.getAbsoluteGravity(edgeGravity, ViewCompat.getLayoutDirection(this)); if (absGrav == Gravity.LEFT) { return mLockModeLeft; } else if (absGrav == Gravity.RIGHT) { return mLockModeRight; } return LOCK_MODE_UNLOCKED; } /** * Check the lock mode of the given drawer view. * * @param drawerView Drawer view to check lock mode * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or * {@link #LOCK_MODE_LOCKED_OPEN}. */ public int getDrawerLockMode(View drawerView) { final int gravity = getDrawerViewGravity(drawerView); if (gravity == Gravity.LEFT) { return mLockModeLeft; } else if (gravity == Gravity.RIGHT) { return mLockModeRight; } return LOCK_MODE_UNLOCKED; } /** * Resolve the shared state of all drawers from the component ViewDragHelpers. * Should be called whenever a ViewDragHelper's state changes. */ void updateDrawerState(int forGravity, int activeState, View activeDrawer) { final int leftState = mLeftDragger.getViewDragState(); final int rightState = mRightDragger.getViewDragState(); final int state; if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) { state = STATE_DRAGGING; } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) { state = STATE_SETTLING; } else { state = STATE_IDLE; } if (activeDrawer != null && activeState == STATE_IDLE) { final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams(); if (lp.onScreen == 0) { dispatchOnDrawerClosed(activeDrawer); } else if (lp.onScreen == 1) { dispatchOnDrawerOpened(activeDrawer); } } if (state != mDrawerState) { mDrawerState = state; if (mListener != null) { mListener.onDrawerStateChanged(state); } } } void dispatchOnDrawerClosed(View drawerView) { final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); if (lp.knownOpen) { lp.knownOpen = false; if (mListener != null) { mListener.onDrawerClosed(drawerView); } sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } } void dispatchOnDrawerOpened(View drawerView) { final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); if (!lp.knownOpen) { lp.knownOpen = true; if (mListener != null) { mListener.onDrawerOpened(drawerView); } drawerView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } } void dispatchOnDrawerSlide(View drawerView, float slideOffset) { if (mListener != null) { mListener.onDrawerSlide(drawerView, slideOffset); } } void setDrawerViewOffset(View drawerView, float slideOffset) { final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); if (slideOffset == lp.onScreen) { return; } lp.onScreen = slideOffset; dispatchOnDrawerSlide(drawerView, slideOffset); } float getDrawerViewOffset(View drawerView) { return ((LayoutParams) drawerView.getLayoutParams()).onScreen; } int getDrawerViewGravity(View drawerView) { final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(drawerView)); } boolean checkDrawerViewGravity(View drawerView, int checkFor) { final int absGrav = getDrawerViewGravity(drawerView); return (absGrav & checkFor) == checkFor; } View findOpenDrawer() { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (((LayoutParams) child.getLayoutParams()).knownOpen) { return child; } } return null; } void moveDrawerToOffset(View drawerView, float slideOffset) { final float oldOffset = getDrawerViewOffset(drawerView); final int width = drawerView.getWidth(); final int oldPos = (int) (width * oldOffset); final int newPos = (int) (width * slideOffset); final int dx = newPos - oldPos; drawerView.offsetLeftAndRight(checkDrawerViewGravity(drawerView, Gravity.LEFT) ? dx : -dx); setDrawerViewOffset(drawerView, slideOffset); } View findDrawerWithGravity(int gravity) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final int childGravity = getDrawerViewGravity(child); if ((childGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == (gravity & Gravity.HORIZONTAL_GRAVITY_MASK)) { return child; } } return null; } /** * Simple gravity to string - only supports LEFT and RIGHT for debugging output. * * @param gravity Absolute gravity value * @return LEFT or RIGHT as appropriate, or a hex string */ static String gravityToString(int gravity) { if ((gravity & Gravity.LEFT) == Gravity.LEFT) { return "LEFT"; } if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { return "RIGHT"; } return Integer.toHexString(gravity); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mFirstLayout = true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mFirstLayout = true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { if (isInEditMode()) { // Don't crash the layout editor. Consume all of the space if specified // or pick a magic number from thin air otherwise. // TODO Better communication with tools of this bogus state. // It will crash on a real device. if (widthMode == MeasureSpec.AT_MOST) { widthMode = MeasureSpec.EXACTLY; } else if (widthMode == MeasureSpec.UNSPECIFIED) { widthMode = MeasureSpec.EXACTLY; widthSize = 300; } if (heightMode == MeasureSpec.AT_MOST) { heightMode = MeasureSpec.EXACTLY; } else if (heightMode == MeasureSpec.UNSPECIFIED) { heightMode = MeasureSpec.EXACTLY; heightSize = 300; } } else { throw new IllegalArgumentException( "DrawerLayout must be measured with MeasureSpec.EXACTLY."); } } setMeasuredDimension(widthSize, heightSize); // Gravity value for each drawer we've seen. Only one of each permitted. int foundDrawers = 0; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (isContentView(child)) { // Content views get measured at exactly the layout's size. final int contentWidthSpec = MeasureSpec.makeMeasureSpec( widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); final int contentHeightSpec = MeasureSpec.makeMeasureSpec( heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); child.measure(contentWidthSpec, contentHeightSpec); } else if (isDrawerView(child)) { final int childGravity = getDrawerViewGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; if ((foundDrawers & childGravity) != 0) { throw new IllegalStateException("Child drawer has absolute gravity " + gravityToString(childGravity) + " but this " + TAG + " already has a " + "drawer view along that edge"); } final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, mMinDrawerMargin + lp.leftMargin + lp.rightMargin, lp.width); final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, lp.topMargin + lp.bottomMargin, lp.height); child.measure(drawerWidthSpec, drawerHeightSpec); } else { throw new IllegalStateException("Child " + child + " at index " + i + " does not have a valid layout_gravity - must be Gravity.LEFT, " + "Gravity.RIGHT or Gravity.NO_GRAVITY"); } } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mInLayout = true; final int width = r - l; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (isContentView(child)) { child.layout(lp.leftMargin, lp.topMargin, lp.leftMargin + child.getMeasuredWidth(), lp.topMargin + child.getMeasuredHeight()); } else { // Drawer, if it wasn't onMeasure would have thrown an exception. final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); int childLeft; final float newOffset; if (checkDrawerViewGravity(child, Gravity.LEFT)) { childLeft = -childWidth + (int) (childWidth * lp.onScreen); newOffset = (float) (childWidth + childLeft) / childWidth; } else { // Right; onMeasure checked for us. childLeft = width - (int) (childWidth * lp.onScreen); newOffset = (float) (width - childLeft) / childWidth; } final boolean changeOffset = newOffset != lp.onScreen; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (vgrav) { default: case Gravity.TOP: { child.layout(childLeft, lp.topMargin, childLeft + childWidth, childHeight); break; } case Gravity.BOTTOM: { final int height = b - t; child.layout(childLeft, height - lp.bottomMargin - child.getMeasuredHeight(), childLeft + childWidth, height - lp.bottomMargin); break; } case Gravity.CENTER_VERTICAL: { final int height = b - t; int childTop = (height - childHeight) / 2; // Offset for margins. If things don't fit right because of // bad measurement before, oh well. if (childTop < lp.topMargin) { childTop = lp.topMargin; } else if (childTop + childHeight > height - lp.bottomMargin) { childTop = height - lp.bottomMargin - childHeight; } child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); break; } } if (changeOffset) { setDrawerViewOffset(child, newOffset); } final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; if (child.getVisibility() != newVisibility) { child.setVisibility(newVisibility); } } } mInLayout = false; mFirstLayout = false; } @Override public void requestLayout() { if (!mInLayout) { super.requestLayout(); } } @Override public void computeScroll() { final int childCount = getChildCount(); float scrimOpacity = 0; for (int i = 0; i < childCount; i++) { final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; scrimOpacity = Math.max(scrimOpacity, onscreen); } mScrimOpacity = scrimOpacity; // "|" used on purpose; both need to run. if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(this); } } private static boolean hasOpaqueBackground(View v) { final Drawable bg = v.getBackground(); if (bg != null) { return bg.getOpacity() == PixelFormat.OPAQUE; } return false; } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final int height = getHeight(); final boolean drawingContent = isContentView(child); int clipLeft = 0, clipRight = getWidth(); final int restoreCount = canvas.save(); if (drawingContent) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View v = getChildAt(i); if (v == child || v.getVisibility() != VISIBLE || !hasOpaqueBackground(v) || !isDrawerView(v) || v.getHeight() < height) { continue; } if (checkDrawerViewGravity(v, Gravity.LEFT)) { final int vright = v.getRight(); if (vright > clipLeft) clipLeft = vright; } else { final int vleft = v.getLeft(); if (vleft < clipRight) clipRight = vleft; } } canvas.clipRect(clipLeft, 0, clipRight, getHeight()); } final boolean result = super.drawChild(canvas, child, drawingTime); canvas.restoreToCount(restoreCount); if (mScrimOpacity > 0 && drawingContent) { final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; final int imag = (int) (baseAlpha * mScrimOpacity); final int color = imag << 24 | (mScrimColor & 0xffffff); mScrimPaint.setColor(color); canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); } else if (mShadowLeft != null && checkDrawerViewGravity(child, Gravity.LEFT)) { final int shadowWidth = mShadowLeft.getIntrinsicWidth(); final int childRight = child.getRight(); final int drawerPeekDistance = mLeftDragger.getEdgeSize(); final float alpha = Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); mShadowLeft.setBounds(childRight, child.getTop(), childRight + shadowWidth, child.getBottom()); mShadowLeft.setAlpha((int) (0xff * alpha)); mShadowLeft.draw(canvas); } else if (mShadowRight != null && checkDrawerViewGravity(child, Gravity.RIGHT)) { final int shadowWidth = mShadowRight.getIntrinsicWidth(); final int childLeft = child.getLeft(); final int showing = getWidth() - childLeft; final int drawerPeekDistance = mRightDragger.getEdgeSize(); final float alpha = Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), childLeft, child.getBottom()); mShadowRight.setAlpha((int) (0xff * alpha)); mShadowRight.draw(canvas); } return result; } boolean isContentView(View child) { return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; } boolean isDrawerView(View child) { final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(child)); return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = MotionEventCompat.getActionMasked(ev); // "|" used deliberately here; both methods should be invoked. final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | mRightDragger.shouldInterceptTouchEvent(ev); boolean interceptForTap = false; switch (action) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); mInitialMotionX = x; mInitialMotionY = y; if (mScrimOpacity > 0 && isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) { interceptForTap = true; } mDisallowInterceptRequested = false; mChildrenCanceledTouch = false; break; } case MotionEvent.ACTION_MOVE: { // If we cross the touch slop, don't perform the delayed peek for an edge touch. if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { mLeftCallback.removeCallbacks(); mRightCallback.removeCallbacks(); } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { closeDrawers(true); mDisallowInterceptRequested = false; mChildrenCanceledTouch = false; } } return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; } @Override public boolean onTouchEvent(MotionEvent ev) { mLeftDragger.processTouchEvent(ev); mRightDragger.processTouchEvent(ev); final int action = ev.getAction(); boolean wantTouchEvents = true; switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); mInitialMotionX = x; mInitialMotionY = y; mDisallowInterceptRequested = false; mChildrenCanceledTouch = false; break; } case MotionEvent.ACTION_UP: { final float x = ev.getX(); final float y = ev.getY(); boolean peekingOnly = true; final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y); if (touchedView != null && isContentView(touchedView)) { final float dx = x - mInitialMotionX; final float dy = y - mInitialMotionY; final int slop = mLeftDragger.getTouchSlop(); if (dx * dx + dy * dy < slop * slop) { // Taps close a dimmed open drawer but only if it isn't locked open. final View openDrawer = findOpenDrawer(); if (openDrawer != null) { peekingOnly = false;//getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN; } } } closeDrawers(peekingOnly); mDisallowInterceptRequested = false; break; } case MotionEvent.ACTION_CANCEL: { closeDrawers(true); mDisallowInterceptRequested = false; mChildrenCanceledTouch = false; break; } } return wantTouchEvents; } public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT)) { // If we have an edge touch we want to skip this and track it for later instead. super.requestDisallowInterceptTouchEvent(disallowIntercept); } mDisallowInterceptRequested = disallowIntercept; if (disallowIntercept) { closeDrawers(true); } } /** * Close all currently open drawer views by animating them out of view. */ public void closeDrawers() { closeDrawers(false); } void closeDrawers(boolean peekingOnly) { boolean needsInvalidate = false; final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { continue; } final int childWidth = child.getWidth(); if (checkDrawerViewGravity(child, Gravity.LEFT)) { needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, -childWidth, child.getTop()); } else { needsInvalidate |= mRightDragger.smoothSlideViewTo(child, getWidth(), child.getTop()); } lp.isPeeking = false; } mLeftCallback.removeCallbacks(); mRightCallback.removeCallbacks(); if (needsInvalidate) { invalidate(); } } /** * Open the specified drawer view by animating it into view. * * @param drawerView Drawer view to open */ public void openDrawer(View drawerView) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); } if (mFirstLayout) { final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); lp.onScreen = 1.f; lp.knownOpen = true; } else { if (checkDrawerViewGravity(drawerView, Gravity.LEFT)) { mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); } else { mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), drawerView.getTop()); } } invalidate(); } /** * Open the specified drawer by animating it out of view. * * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. * GravityCompat.START or GravityCompat.END may also be used. */ public void openDrawer(int gravity) { final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); final View drawerView = findDrawerWithGravity(absGravity); if (drawerView == null) { throw new IllegalArgumentException("No drawer view found with absolute gravity " + gravityToString(absGravity)); } openDrawer(drawerView); } /** * Close the specified drawer view by animating it into view. * * @param drawerView Drawer view to close */ public void closeDrawer(View drawerView) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); } if (mFirstLayout) { final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); lp.onScreen = 0.f; lp.knownOpen = false; } else { if (checkDrawerViewGravity(drawerView, Gravity.LEFT)) { mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), drawerView.getTop()); } else { mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); } } invalidate(); } /** * Close the specified drawer by animating it out of view. * * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. * GravityCompat.START or GravityCompat.END may also be used. */ public void closeDrawer(int gravity) { final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); final View drawerView = findDrawerWithGravity(absGravity); if (drawerView == null) { throw new IllegalArgumentException("No drawer view found with absolute gravity " + gravityToString(absGravity)); } closeDrawer(drawerView); } /** * Check if the given drawer view is currently in an open state. * To be considered "open" the drawer must have settled into its fully * visible state. To check for partial visibility use * {@link #isDrawerVisible(android.view.View)}. * * @param drawer Drawer view to check * @return true if the given drawer view is in an open state * @see #isDrawerVisible(android.view.View) */ public boolean isDrawerOpen(View drawer) { if (!isDrawerView(drawer)) { throw new IllegalArgumentException("View " + drawer + " is not a drawer"); } return ((LayoutParams) drawer.getLayoutParams()).knownOpen; } /** * Check if the given drawer view is currently in an open state. * To be considered "open" the drawer must have settled into its fully * visible state. If there is no drawer with the given gravity this method * will return false. * * @param drawerGravity Gravity of the drawer to check * @return true if the given drawer view is in an open state */ public boolean isDrawerOpen(int drawerGravity) { final View drawerView = findDrawerWithGravity(drawerGravity); if (drawerView != null) { return isDrawerOpen(drawerView); } return false; } /** * Check if a given drawer view is currently visible on-screen. The drawer * may be only peeking onto the screen, fully extended, or anywhere inbetween. * * @param drawer Drawer view to check * @return true if the given drawer is visible on-screen * @see #isDrawerOpen(android.view.View) */ public boolean isDrawerVisible(View drawer) { if (!isDrawerView(drawer)) { throw new IllegalArgumentException("View " + drawer + " is not a drawer"); } return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; } /** * Check if a given drawer view is currently visible on-screen. The drawer * may be only peeking onto the screen, fully extended, or anywhere inbetween. * If there is no drawer with the given gravity this method will return false. * * @param drawerGravity Gravity of the drawer to check * @return true if the given drawer is visible on-screen */ public boolean isDrawerVisible(int drawerGravity) { final View drawerView = findDrawerWithGravity(drawerGravity); if (drawerView != null) { return isDrawerVisible(drawerView); } return false; } private boolean hasPeekingDrawer() { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); if (lp.isPeeking) { return true; } } return false; } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : p instanceof ViewGroup.MarginLayoutParams ? new LayoutParams((MarginLayoutParams) p) : new LayoutParams(p); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams && super.checkLayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } private boolean hasVisibleDrawer() { return findVisibleDrawer() != null; } private View findVisibleDrawer() { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (isDrawerView(child) && isDrawerVisible(child)) { return child; } } return null; } void cancelChildViewTouch() { // Cancel child touches if (!mChildrenCanceledTouch) { final long now = SystemClock.uptimeMillis(); final MotionEvent cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { getChildAt(i).dispatchTouchEvent(cancelEvent); } cancelEvent.recycle(); mChildrenCanceledTouch = true; } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { KeyEventCompat.startTracking(event); return true; } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { final View visibleDrawer = findVisibleDrawer(); if (visibleDrawer != null/* && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED*/) { closeDrawers(); } return visibleDrawer != null; } return super.onKeyUp(keyCode, event); } @Override protected void onRestoreInstanceState(Parcelable state) { final SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); if (ss.openDrawerGravity != Gravity.NO_GRAVITY) { final View toOpen = findDrawerWithGravity(ss.openDrawerGravity); if (toOpen != null) { openDrawer(toOpen); } } setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); } @Override protected Parcelable onSaveInstanceState() { final Parcelable superState = super.onSaveInstanceState(); final SavedState ss = new SavedState(superState); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (!isDrawerView(child)) { continue; } final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp.knownOpen) { ss.openDrawerGravity = lp.gravity; // Only one drawer can be open at a time. break; } } ss.lockModeLeft = mLockModeLeft; ss.lockModeRight = mLockModeRight; return ss; } /** * State persisted across instances */ protected static class SavedState extends BaseSavedState { int openDrawerGravity = Gravity.NO_GRAVITY; int lockModeLeft = LOCK_MODE_UNLOCKED; int lockModeRight = LOCK_MODE_UNLOCKED; public SavedState(Parcel in) { super(in); openDrawerGravity = in.readInt(); } public SavedState(Parcelable superState) { super(superState); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(openDrawerGravity); } public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { @Override public SavedState createFromParcel(Parcel source) { return new SavedState(source); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } private class ViewDragCallback extends ViewDragHelper.Callback { private final int mGravity; private ViewDragHelper mDragger; private final Runnable mPeekRunnable = new Runnable() { @Override public void run() { peekDrawer(); } }; public ViewDragCallback(int gravity) { mGravity = gravity; } public void setDragger(ViewDragHelper dragger) { mDragger = dragger; } public void removeCallbacks() { KlyphDrawerLayout.this.removeCallbacks(mPeekRunnable); } @Override public boolean tryCaptureView(View child, int pointerId) { // Only capture views where the gravity matches what we're looking for. // This lets us use two ViewDragHelpers, one for each side drawer. return isDrawerView(child) && checkDrawerViewGravity(child, mGravity) && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; } @Override public void onViewDragStateChanged(int state) { updateDrawerState(mGravity, state, mDragger.getCapturedView()); } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { float offset; final int childWidth = changedView.getWidth(); // This reverses the positioning shown in onLayout. if (checkDrawerViewGravity(changedView, Gravity.LEFT)) { offset = (float) (childWidth + left) / childWidth; } else { final int width = getWidth(); offset = (float) (width - left) / childWidth; } setDrawerViewOffset(changedView, offset); changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); invalidate(); } @Override public void onViewCaptured(View capturedChild, int activePointerId) { final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); lp.isPeeking = false; closeOtherDrawer(); } private void closeOtherDrawer() { final int otherGrav = mGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; final View toClose = findDrawerWithGravity(otherGrav); if (toClose != null) { closeDrawer(toClose); } } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { // Offset is how open the drawer is, therefore left/right values // are reversed from one another. final float offset = getDrawerViewOffset(releasedChild); final int childWidth = releasedChild.getWidth(); int left; if (checkDrawerViewGravity(releasedChild, Gravity.LEFT)) { left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; } else { final int width = getWidth(); left = xvel < 0 || xvel == 0 && offset < 0.5f ? width - childWidth : width; } mDragger.settleCapturedViewAt(left, releasedChild.getTop()); invalidate(); } @Override public void onEdgeTouched(int edgeFlags, int pointerId) { postDelayed(mPeekRunnable, PEEK_DELAY); } private void peekDrawer() { final View toCapture; final int childLeft; final int peekDistance = mDragger.getEdgeSize(); final boolean leftEdge = mGravity == Gravity.LEFT; if (leftEdge) { toCapture = findDrawerWithGravity(Gravity.LEFT); childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; } else { toCapture = findDrawerWithGravity(Gravity.RIGHT); childLeft = getWidth() - peekDistance; } // Only peek if it would mean making the drawer more visible and the drawer isn't locked if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || (!leftEdge && toCapture.getLeft() > childLeft)) && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); lp.isPeeking = true; invalidate(); closeOtherDrawer(); cancelChildViewTouch(); } } @Override public boolean onEdgeLock(int edgeFlags) { if (ALLOW_EDGE_LOCK) { final View drawer = findDrawerWithGravity(mGravity); if (drawer != null && !isDrawerOpen(drawer)) { closeDrawer(drawer); } return true; } return false; } @Override public void onEdgeDragStarted(int edgeFlags, int pointerId) { final View toCapture; if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { toCapture = findDrawerWithGravity(Gravity.LEFT); } else { toCapture = findDrawerWithGravity(Gravity.RIGHT); } if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { mDragger.captureChildView(toCapture, pointerId); } } @Override public int getViewHorizontalDragRange(View child) { return child.getWidth(); } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (checkDrawerViewGravity(child, Gravity.LEFT)) { return Math.max(-child.getWidth(), Math.min(left, 0)); } else { final int width = getWidth(); return Math.max(width - child.getWidth(), Math.min(left, width)); } } @Override public int clampViewPositionVertical(View child, int top, int dy) { return child.getTop(); } } public static class LayoutParams extends ViewGroup.MarginLayoutParams { public int gravity = Gravity.NO_GRAVITY; float onScreen; boolean isPeeking; boolean knownOpen; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); this.gravity = a.getInt(0, Gravity.NO_GRAVITY); a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int width, int height, int gravity) { this(width, height); this.gravity = gravity; } public LayoutParams(LayoutParams source) { super(source); this.gravity = source.gravity; } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } public LayoutParams(ViewGroup.MarginLayoutParams source) { super(source); } } class AccessibilityDelegate extends AccessibilityDelegateCompat { private final Rect mTmpRect = new Rect(); @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); super.onInitializeAccessibilityNodeInfo(host, superNode); info.setSource(host); final ViewParent parent = ViewCompat.getParentForAccessibility(host); if (parent instanceof View) { info.setParent((View) parent); } copyNodeInfoNoChildren(info, superNode); superNode.recycle(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (!filter(child)) { info.addChild(child); } } } @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) { if (!filter(child)) { return super.onRequestSendAccessibilityEvent(host, child, event); } return false; } public boolean filter(View child) { final View openDrawer = findOpenDrawer(); return openDrawer != null && openDrawer != child; } /** * This should really be in AccessibilityNodeInfoCompat, but there unfortunately * seem to be a few elements that are not easily cloneable using the underlying API. * Leave it private here as it's not general-purpose useful. */ private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, AccessibilityNodeInfoCompat src) { final Rect rect = mTmpRect; src.getBoundsInParent(rect); dest.setBoundsInParent(rect); src.getBoundsInScreen(rect); dest.setBoundsInScreen(rect); dest.setVisibleToUser(src.isVisibleToUser()); dest.setPackageName(src.getPackageName()); dest.setClassName(src.getClassName()); dest.setContentDescription(src.getContentDescription()); dest.setEnabled(src.isEnabled()); dest.setClickable(src.isClickable()); dest.setFocusable(src.isFocusable()); dest.setFocused(src.isFocused()); dest.setAccessibilityFocused(src.isAccessibilityFocused()); dest.setSelected(src.isSelected()); dest.setLongClickable(src.isLongClickable()); dest.addAction(src.getActions()); } } }