/* * 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 android.support.v4.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.annotation.DrawableRes; import android.support.annotation.IntDef; import android.support.annotation.Nullable; 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.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.ViewGroup.LayoutParams; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; /** * 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. * *
Drawer positioning and layout is controlled using the android:layout_gravity
* 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.)
*
To use a DrawerLayout, position your primary content view as the first child with
* a width and height of match_parent
. Add drawers as child views after the main
* content view and set the layout_gravity
appropriately. Drawers commonly use
* match_parent
for height with a fixed width.
{@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.
* *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.
* *For more information about how to use DrawerLayout, read Creating a Navigation * Drawer.
*/ public class DrawerLayout extends ViewGroup { private static final String TAG = "DrawerLayout"; /** @hide */ @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING}) @Retention(RetentionPolicy.SOURCE) private @interface State {} /** * 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; /** @hide */ @IntDef({LOCK_MODE_UNLOCKED, LOCK_MODE_LOCKED_CLOSED, LOCK_MODE_LOCKED_OPEN}) @Retention(RetentionPolicy.SOURCE) private @interface LockMode {} /** * 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; /** @hide */ @IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END}) @Retention(RetentionPolicy.SOURCE) private @interface EdgeGravity {} 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 boolean CHILDREN_DISALLOW_INTERCEPT = true; private static final float TOUCH_SLOP_SENSITIVITY = 1.f; private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_gravity }; private final ChildAccessibilityDelegate mChildAccessibilityDelegate = new ChildAccessibilityDelegate(); 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 DrawerListener mListener; private float mInitialMotionX; private float mInitialMotionY; private Drawable mShadowLeft; private Drawable mShadowRight; private CharSequence mTitleLeft; private CharSequence mTitleRight; /** * 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(@State 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 DrawerLayout(Context context) { this(context, null); } public DrawerLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DrawerLayout(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, TOUCH_SLOP_SENSITIVITY, mLeftCallback); mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); mLeftDragger.setMinVelocity(minVel); mLeftCallback.setDragger(mLeftDragger); mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback); mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); mRightDragger.setMinVelocity(minVel); mRightCallback.setDragger(mRightDragger); // So that we can catch the back button setFocusableInTouchMode(true); ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 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, @EdgeGravity 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(@DrawableRes int resId, @EdgeGravity 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(DrawerListener listener) { mListener = listener; } /** * Enable or disable interaction with all drawers. * *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.
* *Locking drawers open or closed will implicitly open or close * any drawers as appropriate.
* * @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(@LockMode int lockMode) { setDrawerLockMode(lockMode, Gravity.LEFT); setDrawerLockMode(lockMode, Gravity.RIGHT); } /** * Enable or disable interaction with the given drawer. * *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.
* *Locking a drawer open or closed will implicitly open or close * that drawer as appropriate.
* * @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(@LockMode int lockMode, @EdgeGravity int edgeGravity) { final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity, ViewCompat.getLayoutDirection(this)); if (absGravity == Gravity.LEFT) { mLockModeLeft = lockMode; } else if (absGravity == Gravity.RIGHT) { mLockModeRight = lockMode; } if (lockMode != LOCK_MODE_UNLOCKED) { // Cancel interaction in progress final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger; helper.cancel(); } switch (lockMode) { case LOCK_MODE_LOCKED_OPEN: final View toOpen = findDrawerWithGravity(absGravity); if (toOpen != null) { openDrawer(toOpen); } break; case LOCK_MODE_LOCKED_CLOSED: final View toClose = findDrawerWithGravity(absGravity); if (toClose != null) { closeDrawer(toClose); } break; // default: do nothing } } /** * Enable or disable interaction with the given drawer. * *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.
* *Locking a drawer open or closed will implicitly open or close * that drawer as appropriate.
* * @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(@LockMode int lockMode, View drawerView) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a " + "drawer with appropriate layout_gravity"); } final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; setDrawerLockMode(lockMode, gravity); } /** * 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}. */ @LockMode public int getDrawerLockMode(@EdgeGravity int edgeGravity) { final int absGravity = GravityCompat.getAbsoluteGravity( edgeGravity, ViewCompat.getLayoutDirection(this)); if (absGravity == Gravity.LEFT) { return mLockModeLeft; } else if (absGravity == 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}. */ @LockMode public int getDrawerLockMode(View drawerView) { final int absGravity = getDrawerViewAbsoluteGravity(drawerView); if (absGravity == Gravity.LEFT) { return mLockModeLeft; } else if (absGravity == Gravity.RIGHT) { return mLockModeRight; } return LOCK_MODE_UNLOCKED; } /** * Sets the title of the drawer with the given gravity. *
* When accessibility is turned on, this is the title that will be used to
* identify the drawer to the active accessibility service.
*
* @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which
* drawer to set the title for.
* @param title The title for the drawer.
*/
public void setDrawerTitle(@EdgeGravity int edgeGravity, CharSequence title) {
final int absGravity = GravityCompat.getAbsoluteGravity(
edgeGravity, ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
mTitleLeft = title;
} else if (absGravity == Gravity.RIGHT) {
mTitleRight = title;
}
}
/**
* Returns the title of the drawer with the given gravity.
*
* @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which
* drawer to return the title for.
* @return The title of the drawer, or null if none set.
* @see #setDrawerTitle(int, CharSequence)
*/
@Nullable
public CharSequence getDrawerTitle(@EdgeGravity int edgeGravity) {
final int absGravity = GravityCompat.getAbsoluteGravity(
edgeGravity, ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
return mTitleLeft;
} else if (absGravity == Gravity.RIGHT) {
return mTitleRight;
}
return null;
}
/**
* Resolve the shared state of all drawers from the component ViewDragHelpers.
* Should be called whenever a ViewDragHelper's state changes.
*/
void updateDrawerState(int forGravity, @State 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);
}
// If no drawer is opened, all drawers are not shown
// for accessibility and the content is shown.
View content = getChildAt(0);
if (content != null) {
ViewCompat.setImportantForAccessibility(content,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
ViewCompat.setImportantForAccessibility(drawerView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
// Only send WINDOW_STATE_CHANGE if the host has window focus. This
// may change if support for multiple foreground windows (e.g. IME)
// improves.
if (hasWindowFocus()) {
final View rootView = getRootView();
if (rootView != null) {
rootView.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);
}
// If a drawer is opened, only it is shown for
// accessibility and the content is not shown.
View content = getChildAt(0);
if (content != null) {
ViewCompat.setImportantForAccessibility(content,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
}
ViewCompat.setImportantForAccessibility(drawerView,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
drawerView.requestFocus();
}
}
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;
}
/**
* @return the absolute gravity of the child drawerView, resolved according
* to the current layout direction
*/
int getDrawerViewAbsoluteGravity(View drawerView) {
final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this));
}
boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) {
final int absGravity = getDrawerViewAbsoluteGravity(drawerView);
return (absGravity & 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(
checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx);
setDrawerViewOffset(drawerView, slideOffset);
}
/**
* @param gravity the gravity of the child to return. If specified as a
* relative value, it will be resolved according to the current
* layout direction.
* @return the drawer with the specified gravity
*/
View findDrawerWithGravity(int gravity) {
final int absHorizGravity = GravityCompat.getAbsoluteGravity(
gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final int childAbsGravity = getDrawerViewAbsoluteGravity(child);
if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) {
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(@EdgeGravity 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 =
getDrawerViewAbsoluteGravity(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 (checkDrawerViewAbsoluteGravity(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,
lp.topMargin + 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 (checkDrawerViewAbsoluteGravity(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 && checkDrawerViewAbsoluteGravity(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 && checkDrawerViewAbsoluteGravity(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 = 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 (CHILDREN_DISALLOW_INTERCEPT ||
(!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 (checkDrawerViewAbsoluteGravity(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 (checkDrawerViewAbsoluteGravity(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(@EdgeGravity int gravity) {
final View drawerView = findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
gravityToString(gravity));
}
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 (checkDrawerViewAbsoluteGravity(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(@EdgeGravity int gravity) {
final View drawerView = findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
gravityToString(gravity));
}
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(@EdgeGravity 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 in between.
* 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(@EdgeGravity 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;
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
// Until a drawer is open, it is hidden from accessibility.
if (index > 0 || (index < 0 && getChildCount() > 0)) {
ViewCompat.setImportantForAccessibility(child,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
// Also set a delegate to break the child-parent relation if the
// child is hidden. For details (see incluceChildForAccessibility).
ViewCompat.setAccessibilityDelegate(child, mChildAccessibilityDelegate);
} else {
// Initially, the content is shown for accessibility.
ViewCompat.setImportantForAccessibility(child,
ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
super.addView(child, index, params);
}
private static boolean includeChildForAccessibilitiy(View child) {
// If the child is not important for accessibility we make
// sure this hides the entire subtree rooted at it as the
// IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDATS is not
// supported on older platforms but we want to hide the entire
// content and not opened drawers if a drawer is opened.
return ViewCompat.getImportantForAccessibility(child)
!= ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
&& ViewCompat.getImportantForAccessibility(child)
!= ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO;
}
/**
* 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