/* * 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.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.support.annotation.ColorInt; import android.support.annotation.DrawableRes; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v4.os.ParcelableCompat; import android.support.v4.os.ParcelableCompatCreatorCallbacks; import android.support.v4.view.AbsSavedState; import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.GravityCompat; 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.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; 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; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; 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 one or both vertical edges 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.) * Note that you can only have one drawer view for each vertical edge of the window. If your * layout configures more than one drawer view per vertical edge of the window, an exception will * be thrown at runtime. *

* *

To use a DrawerLayout, position your primary content view as the first child with * width and height of match_parent and no layout_gravity>. * 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 implements DrawerLayoutImpl { private static final String TAG = "DrawerLayout"; @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; @IntDef({LOCK_MODE_UNLOCKED, LOCK_MODE_LOCKED_CLOSED, LOCK_MODE_LOCKED_OPEN, LOCK_MODE_UNDEFINED}) @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; /** * The drawer's lock state is reset to default. */ public static final int LOCK_MODE_UNDEFINED = 3; @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 DRAWER_ELEVATION = 10; //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; static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_gravity }; /** Whether we can use NO_HIDE_DESCENDANTS accessibility importance. */ static final boolean CAN_HIDE_DESCENDANTS = Build.VERSION.SDK_INT >= 19; /** Whether the drawer shadow comes from setting elevation on the drawer. */ private static final boolean SET_DRAWER_SHADOW_FROM_ELEVATION = Build.VERSION.SDK_INT >= 21; private final ChildAccessibilityDelegate mChildAccessibilityDelegate = new ChildAccessibilityDelegate(); private float mDrawerElevation; 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 @LockMode int mLockModeLeft = LOCK_MODE_UNDEFINED; private @LockMode int mLockModeRight = LOCK_MODE_UNDEFINED; private @LockMode int mLockModeStart = LOCK_MODE_UNDEFINED; private @LockMode int mLockModeEnd = LOCK_MODE_UNDEFINED; private boolean mDisallowInterceptRequested; private boolean mChildrenCanceledTouch; private @Nullable DrawerListener mListener; private List mListeners; private float mInitialMotionX; private float mInitialMotionY; private Drawable mStatusBarBackground; private Drawable mShadowLeftResolved; private Drawable mShadowRightResolved; private CharSequence mTitleLeft; private CharSequence mTitleRight; private Object mLastInsets; private boolean mDrawStatusBarBackground; /** Shadow drawables for different gravity */ private Drawable mShadowStart = null; private Drawable mShadowEnd = null; private Drawable mShadowLeft = null; private Drawable mShadowRight = null; private final ArrayList mNonDrawerViews; /** * 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 */ 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 */ void onDrawerOpened(View drawerView); /** * Called when a drawer has settled in a completely closed state. * * @param drawerView Drawer view that is now closed */ 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 */ 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 abstract static 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) { } } interface DrawerLayoutCompatImpl { void configureApplyInsets(View drawerLayout); void dispatchChildInsets(View child, Object insets, int drawerGravity); void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity); int getTopInset(Object lastInsets); Drawable getDefaultStatusBarBackground(Context context); } static class DrawerLayoutCompatImplBase implements DrawerLayoutCompatImpl { @Override public void configureApplyInsets(View drawerLayout) { // This space for rent } @Override public void dispatchChildInsets(View child, Object insets, int drawerGravity) { // This space for rent } @Override public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) { // This space for rent } @Override public int getTopInset(Object insets) { return 0; } @Override public Drawable getDefaultStatusBarBackground(Context context) { return null; } } static class DrawerLayoutCompatImplApi21 implements DrawerLayoutCompatImpl { @Override public void configureApplyInsets(View drawerLayout) { DrawerLayoutCompatApi21.configureApplyInsets(drawerLayout); } @Override public void dispatchChildInsets(View child, Object insets, int drawerGravity) { DrawerLayoutCompatApi21.dispatchChildInsets(child, insets, drawerGravity); } @Override public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) { DrawerLayoutCompatApi21.applyMarginInsets(lp, insets, drawerGravity); } @Override public int getTopInset(Object insets) { return DrawerLayoutCompatApi21.getTopInset(insets); } @Override public Drawable getDefaultStatusBarBackground(Context context) { return DrawerLayoutCompatApi21.getDefaultStatusBarBackground(context); } } static { final int version = Build.VERSION.SDK_INT; if (version >= 21) { IMPL = new DrawerLayoutCompatImplApi21(); } else { IMPL = new DrawerLayoutCompatImplBase(); } } static final DrawerLayoutCompatImpl IMPL; 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); setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 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); if (ViewCompat.getFitsSystemWindows(this)) { IMPL.configureApplyInsets(this); mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context); } mDrawerElevation = DRAWER_ELEVATION * density; mNonDrawerViews = new ArrayList(); } /** * Sets the base elevation of the drawer(s) relative to the parent, in pixels. Note that the * elevation change is only supported in API 21 and above. * * @param elevation The base depth position of the view, in pixels. */ public void setDrawerElevation(float elevation) { mDrawerElevation = elevation; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (isDrawerView(child)) { ViewCompat.setElevation(child, mDrawerElevation); } } } /** * The base elevation of the drawer(s) relative to the parent, in pixels. Note that the * elevation change is only supported in API 21 and above. For unsupported API levels, 0 will * be returned as the elevation. * * @return The base depth position of the view, in pixels. */ public float getDrawerElevation() { if (SET_DRAWER_SHADOW_FROM_ELEVATION) { return mDrawerElevation; } return 0f; } /** * @hide Internal use only; called to apply window insets when configured * with fitsSystemWindows="true" */ @Override public void setChildInsets(Object insets, boolean draw) { mLastInsets = insets; mDrawStatusBarBackground = draw; setWillNotDraw(!draw && getBackground() == null); requestLayout(); } /** * Set a simple drawable used for the left or right shadow. The drawable provided must have a * nonzero intrinsic width. For API 21 and above, an elevation will be set on the drawer * instead of the drawable provided. * *

Note that for better support for both left-to-right and right-to-left layout * directions, a drawable for RTL layout (in additional to the one in LTR layout) can be * defined with a resource qualifier "ldrtl" for API 17 and above with the gravity * {@link GravityCompat#START}. Alternatively, for API 23 and above, the drawable can * auto-mirrored such that the drawable will be mirrored in RTL layout.

* * @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. */ if (SET_DRAWER_SHADOW_FROM_ELEVATION) { // No op. Drawer shadow will come from setting an elevation on the drawer. return; } if ((gravity & GravityCompat.START) == GravityCompat.START) { mShadowStart = shadowDrawable; } else if ((gravity & GravityCompat.END) == GravityCompat.END) { mShadowEnd = shadowDrawable; } else if ((gravity & Gravity.LEFT) == Gravity.LEFT) { mShadowLeft = shadowDrawable; } else if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { mShadowRight = shadowDrawable; } else { return; } resolveShadowDrawables(); invalidate(); } /** * Set a simple drawable used for the left or right shadow. The drawable provided must have a * nonzero intrinsic width. For API 21 and above, an elevation will be set on the drawer * instead of the drawable provided. * *

Note that for better support for both left-to-right and right-to-left layout * directions, a drawable for RTL layout (in additional to the one in LTR layout) can be * defined with a resource qualifier "ldrtl" for API 17 and above with the gravity * {@link GravityCompat#START}. Alternatively, for API 23 and above, the drawable can * auto-mirrored such that the drawable will be mirrored in RTL layout.

* * @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(ContextCompat.getDrawable(getContext(), 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(@ColorInt int color) { mScrimColor = color; invalidate(); } /** * Set a listener to be notified of drawer events. Note that this method is deprecated * and you should use {@link #addDrawerListener(DrawerListener)} to add a listener and * {@link #removeDrawerListener(DrawerListener)} to remove a registered listener. * * @param listener Listener to notify when drawer events occur * @deprecated Use {@link #addDrawerListener(DrawerListener)} * @see DrawerListener * @see #addDrawerListener(DrawerListener) * @see #removeDrawerListener(DrawerListener) */ @Deprecated public void setDrawerListener(DrawerListener listener) { // The logic in this method emulates what we had before support for multiple // registered listeners. if (mListener != null) { removeDrawerListener(mListener); } if (listener != null) { addDrawerListener(listener); } // Update the deprecated field so that we can remove the passed listener the next // time we're called mListener = listener; } /** * Adds the specified listener to the list of listeners that will be notified of drawer events. * * @param listener Listener to notify when drawer events occur. * @see #removeDrawerListener(DrawerListener) */ public void addDrawerListener(@NonNull DrawerListener listener) { if (listener == null) { return; } if (mListeners == null) { mListeners = new ArrayList(); } mListeners.add(listener); } /** * Removes the specified listener from the list of listeners that will be notified of drawer * events. * * @param listener Listener to remove from being notified of drawer events * @see #addDrawerListener(DrawerListener) */ public void removeDrawerListener(@NonNull DrawerListener listener) { if (listener == null) { return; } if (mListeners == null) { // This can happen if this method is called before the first call to addDrawerListener return; } mListeners.remove(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)); switch (edgeGravity) { case Gravity.LEFT: mLockModeLeft = lockMode; break; case Gravity.RIGHT: mLockModeRight = lockMode; break; case GravityCompat.START: mLockModeStart = lockMode; break; case GravityCompat.END: mLockModeEnd = lockMode; break; } 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) { int layoutDirection = ViewCompat.getLayoutDirection(this); switch (edgeGravity) { case Gravity.LEFT: if (mLockModeLeft != LOCK_MODE_UNDEFINED) { return mLockModeLeft; } int leftLockMode = (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) ? mLockModeStart : mLockModeEnd; if (leftLockMode != LOCK_MODE_UNDEFINED) { return leftLockMode; } break; case Gravity.RIGHT: if (mLockModeRight != LOCK_MODE_UNDEFINED) { return mLockModeRight; } int rightLockMode = (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) ? mLockModeEnd : mLockModeStart; if (rightLockMode != LOCK_MODE_UNDEFINED) { return rightLockMode; } break; case GravityCompat.START: if (mLockModeStart != LOCK_MODE_UNDEFINED) { return mLockModeStart; } int startLockMode = (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) ? mLockModeLeft : mLockModeRight; if (startLockMode != LOCK_MODE_UNDEFINED) { return startLockMode; } break; case GravityCompat.END: if (mLockModeEnd != LOCK_MODE_UNDEFINED) { return mLockModeEnd; } int endLockMode = (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) ? mLockModeRight : mLockModeLeft; if (endLockMode != LOCK_MODE_UNDEFINED) { return endLockMode; } break; } 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) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a drawer"); } final int drawerGravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; return getDrawerLockMode(drawerGravity); } /** * 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 (mListeners != null) { // Notify the listeners. Do that from the end of the list so that if a listener // removes itself as the result of being called, it won't mess up with our iteration int listenerCount = mListeners.size(); for (int i = listenerCount - 1; i >= 0; i--) { mListeners.get(i).onDrawerStateChanged(state); } } } } void dispatchOnDrawerClosed(View drawerView) { final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); if ((lp.openState & LayoutParams.FLAG_IS_OPENED) == 1) { lp.openState = 0; if (mListeners != null) { // Notify the listeners. Do that from the end of the list so that if a listener // removes itself as the result of being called, it won't mess up with our iteration int listenerCount = mListeners.size(); for (int i = listenerCount - 1; i >= 0; i--) { mListeners.get(i).onDrawerClosed(drawerView); } } updateChildrenImportantForAccessibility(drawerView, false); // 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.openState & LayoutParams.FLAG_IS_OPENED) == 0) { lp.openState = LayoutParams.FLAG_IS_OPENED; if (mListeners != null) { // Notify the listeners. Do that from the end of the list so that if a listener // removes itself as the result of being called, it won't mess up with our iteration int listenerCount = mListeners.size(); for (int i = listenerCount - 1; i >= 0; i--) { mListeners.get(i).onDrawerOpened(drawerView); } } updateChildrenImportantForAccessibility(drawerView, true); // Only send WINDOW_STATE_CHANGE if the host has window focus. if (hasWindowFocus()) { sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } } } private void updateChildrenImportantForAccessibility(View drawerView, boolean isDrawerOpen) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (!isDrawerOpen && !isDrawerView(child) || isDrawerOpen && child == drawerView) { // Drawer is closed and this is a content view or this is an // open drawer view, so it should be visible. ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } else { ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } } } void dispatchOnDrawerSlide(View drawerView, float slideOffset) { if (mListeners != null) { // Notify the listeners. Do that from the end of the list so that if a listener // removes itself as the result of being called, it won't mess up with our iteration int listenerCount = mListeners.size(); for (int i = listenerCount - 1; i >= 0; i--) { mListeners.get(i).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); final LayoutParams childLp = (LayoutParams) child.getLayoutParams(); if ((childLp.openState & LayoutParams.FLAG_IS_OPENED) == 1) { 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); final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this); final int layoutDirection = ViewCompat.getLayoutDirection(this); // Only one drawer is permitted along each vertical edge (left / right). These two booleans // are tracking the presence of the edge drawers. boolean hasDrawerOnLeftEdge = false; boolean hasDrawerOnRightEdge = false; 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 (applyInsets) { final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection); if (ViewCompat.getFitsSystemWindows(child)) { IMPL.dispatchChildInsets(child, mLastInsets, cgrav); } else { IMPL.applyMarginInsets(lp, mLastInsets, cgrav); } } 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)) { if (SET_DRAWER_SHADOW_FROM_ELEVATION) { if (ViewCompat.getElevation(child) != mDrawerElevation) { ViewCompat.setElevation(child, mDrawerElevation); } } final @EdgeGravity int childGravity = getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; // Note that the isDrawerView check guarantees that childGravity here is either // LEFT or RIGHT boolean isLeftEdgeDrawer = (childGravity == Gravity.LEFT); if ((isLeftEdgeDrawer && hasDrawerOnLeftEdge) || (!isLeftEdgeDrawer && hasDrawerOnRightEdge)) { throw new IllegalStateException("Child drawer has absolute gravity " + gravityToString(childGravity) + " but this " + TAG + " already has a " + "drawer view along that edge"); } if (isLeftEdgeDrawer) { hasDrawerOnLeftEdge = true; } else { hasDrawerOnRightEdge = true; } 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"); } } } private void resolveShadowDrawables() { if (SET_DRAWER_SHADOW_FROM_ELEVATION) { return; } mShadowLeftResolved = resolveLeftShadow(); mShadowRightResolved = resolveRightShadow(); } private Drawable resolveLeftShadow() { int layoutDirection = ViewCompat.getLayoutDirection(this); // Prefer shadows defined with start/end gravity over left and right. if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { if (mShadowStart != null) { // Correct drawable layout direction, if needed. mirror(mShadowStart, layoutDirection); return mShadowStart; } } else { if (mShadowEnd != null) { // Correct drawable layout direction, if needed. mirror(mShadowEnd, layoutDirection); return mShadowEnd; } } return mShadowLeft; } private Drawable resolveRightShadow() { int layoutDirection = ViewCompat.getLayoutDirection(this); if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_LTR) { if (mShadowEnd != null) { // Correct drawable layout direction, if needed. mirror(mShadowEnd, layoutDirection); return mShadowEnd; } } else { if (mShadowStart != null) { // Correct drawable layout direction, if needed. mirror(mShadowStart, layoutDirection); return mShadowStart; } } return mShadowRight; } /** * Change the layout direction of the given drawable. * Return true if auto-mirror is supported and drawable's layout direction can be changed. * Otherwise, return false. */ private boolean mirror(Drawable drawable, int layoutDirection) { if (drawable == null || !DrawableCompat.isAutoMirrored(drawable)) { return false; } DrawableCompat.setLayoutDirection(drawable, layoutDirection); return true; } @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; } /** * Set a drawable to draw in the insets area for the status bar. * Note that this will only be activated if this DrawerLayout fitsSystemWindows. * * @param bg Background drawable to draw behind the status bar */ public void setStatusBarBackground(Drawable bg) { mStatusBarBackground = bg; invalidate(); } /** * Gets the drawable used to draw in the insets area for the status bar. * * @return The status bar background drawable, or null if none set */ public Drawable getStatusBarBackgroundDrawable() { return mStatusBarBackground; } /** * Set a drawable to draw in the insets area for the status bar. * Note that this will only be activated if this DrawerLayout fitsSystemWindows. * * @param resId Resource id of a background drawable to draw behind the status bar */ public void setStatusBarBackground(int resId) { mStatusBarBackground = resId != 0 ? ContextCompat.getDrawable(getContext(), resId) : null; invalidate(); } /** * Set a drawable to draw in the insets area for the status bar. * Note that this will only be activated if this DrawerLayout fitsSystemWindows. * * @param color Color to use as a background drawable to draw behind the status bar * in 0xAARRGGBB format. */ public void setStatusBarBackgroundColor(@ColorInt int color) { mStatusBarBackground = new ColorDrawable(color); invalidate(); } public void onRtlPropertiesChanged(int layoutDirection) { resolveShadowDrawables(); } @Override public void onDraw(Canvas c) { super.onDraw(c); if (mDrawStatusBarBackground && mStatusBarBackground != null) { final int inset = IMPL.getTopInset(mLastInsets); if (inset > 0) { mStatusBarBackground.setBounds(0, 0, getWidth(), inset); mStatusBarBackground.draw(c); } } } @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 (mShadowLeftResolved != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { final int shadowWidth = mShadowLeftResolved.getIntrinsicWidth(); final int childRight = child.getRight(); final int drawerPeekDistance = mLeftDragger.getEdgeSize(); final float alpha = Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); mShadowLeftResolved.setBounds(childRight, child.getTop(), childRight + shadowWidth, child.getBottom()); mShadowLeftResolved.setAlpha((int) (0xff * alpha)); mShadowLeftResolved.draw(canvas); } else if (mShadowRightResolved != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { final int shadowWidth = mShadowRightResolved.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)); mShadowRightResolved.setBounds(childLeft - shadowWidth, child.getTop(), childLeft, child.getBottom()); mShadowRightResolved.setAlpha((int) (0xff * alpha)); mShadowRightResolved.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)); if ((absGravity & Gravity.LEFT) != 0) { // This child is a left-edge drawer return true; } if ((absGravity & Gravity.RIGHT) != 0) { // This child is a right-edge drawer return true; } return false; } @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) { final View child = mLeftDragger.findTopChildUnder((int) x, (int) y); if (child != null && isContentView(child)) { 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; } @Override 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) { openDrawer(drawerView, true); } /** * Open the specified drawer view. * * @param drawerView Drawer view to open * @param animate Whether opening of the drawer should be animated. */ public void openDrawer(View drawerView, boolean animate) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); } final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); if (mFirstLayout) { lp.onScreen = 1.f; lp.openState = LayoutParams.FLAG_IS_OPENED; updateChildrenImportantForAccessibility(drawerView, true); } else if (animate) { lp.openState |= LayoutParams.FLAG_IS_OPENING; if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); } else { mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), drawerView.getTop()); } } else { moveDrawerToOffset(drawerView, 1.f); updateDrawerState(lp.gravity, STATE_IDLE, drawerView); drawerView.setVisibility(VISIBLE); } 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) { openDrawer(gravity, true); } /** * Open the specified drawer. * * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. * GravityCompat.START or GravityCompat.END may also be used. * @param animate Whether opening of the drawer should be animated. */ public void openDrawer(@EdgeGravity int gravity, boolean animate) { final View drawerView = findDrawerWithGravity(gravity); if (drawerView == null) { throw new IllegalArgumentException("No drawer view found with gravity " + gravityToString(gravity)); } openDrawer(drawerView, animate); } /** * Close the specified drawer view by animating it into view. * * @param drawerView Drawer view to close */ public void closeDrawer(View drawerView) { closeDrawer(drawerView, true); } /** * Close the specified drawer view. * * @param drawerView Drawer view to close * @param animate Whether closing of the drawer should be animated. */ public void closeDrawer(View drawerView, boolean animate) { if (!isDrawerView(drawerView)) { throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); } final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); if (mFirstLayout) { lp.onScreen = 0.f; lp.openState = 0; } else if (animate) { lp.openState |= LayoutParams.FLAG_IS_CLOSING; if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), drawerView.getTop()); } else { mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); } } else { moveDrawerToOffset(drawerView, 0.f); updateDrawerState(lp.gravity, STATE_IDLE, drawerView); drawerView.setVisibility(INVISIBLE); } 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) { closeDrawer(gravity, true); } /** * Close the specified drawer. * * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. * GravityCompat.START or GravityCompat.END may also be used. * @param animate Whether closing of the drawer should be animated. */ public void closeDrawer(@EdgeGravity int gravity, boolean animate) { final View drawerView = findDrawerWithGravity(gravity); if (drawerView == null) { throw new IllegalArgumentException("No drawer view found with gravity " + gravityToString(gravity)); } closeDrawer(drawerView, animate); } /** * 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"); } LayoutParams drawerLp = (LayoutParams) drawer.getLayoutParams(); return (drawerLp.openState & LayoutParams.FLAG_IS_OPENED) == 1; } /** * 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.MATCH_PARENT, LayoutParams.MATCH_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); } @Override public void addFocusables(ArrayList views, int direction, int focusableMode) { if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { return; } // Only the views in the open drawers are focusables. Add normal child views when // no drawers are opened. final int childCount = getChildCount(); boolean isDrawerOpen = false; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (isDrawerView(child)) { if (isDrawerOpen(child)) { isDrawerOpen = true; child.addFocusables(views, direction, focusableMode); } } else { mNonDrawerViews.add(child); } } if (!isDrawerOpen) { final int nonDrawerViewsCount = mNonDrawerViews.size(); for (int i = 0; i < nonDrawerViewsCount; ++i) { final View child = mNonDrawerViews.get(i); if (child.getVisibility() == View.VISIBLE) { child.addFocusables(views, direction, focusableMode); } } } mNonDrawerViews.clear(); } private boolean hasVisibleDrawer() { return findVisibleDrawer() != null; } 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()) { event.startTracking(); 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) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } 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); } } if (ss.lockModeLeft != LOCK_MODE_UNDEFINED) { setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); } if (ss.lockModeRight != LOCK_MODE_UNDEFINED) { setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); } if (ss.lockModeStart != LOCK_MODE_UNDEFINED) { setDrawerLockMode(ss.lockModeStart, GravityCompat.START); } if (ss.lockModeEnd != LOCK_MODE_UNDEFINED) { setDrawerLockMode(ss.lockModeEnd, GravityCompat.END); } } @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); LayoutParams lp = (LayoutParams) child.getLayoutParams(); // Is the current child fully opened (that is, not closing)? boolean isOpenedAndNotClosing = (lp.openState == LayoutParams.FLAG_IS_OPENED); // Is the current child opening? boolean isClosedAndOpening = (lp.openState == LayoutParams.FLAG_IS_OPENING); if (isOpenedAndNotClosing || isClosedAndOpening) { // If one of the conditions above holds, save the child's gravity // so that we open that child during state restore. ss.openDrawerGravity = lp.gravity; break; } } ss.lockModeLeft = mLockModeLeft; ss.lockModeRight = mLockModeRight; ss.lockModeStart = mLockModeStart; ss.lockModeEnd = mLockModeEnd; return ss; } @Override public void addView(View child, int index, ViewGroup.LayoutParams params) { super.addView(child, index, params); final View openDrawer = findOpenDrawer(); if (openDrawer != null || isDrawerView(child)) { // A drawer is already open or the new view is a drawer, so the // new view should start out hidden. ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } else { // Otherwise this is a content view and no drawer is open, so the // new view should start out visible. ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } // We only need a delegate here if the framework doesn't understand // NO_HIDE_DESCENDANTS importance. if (!CAN_HIDE_DESCENDANTS) { ViewCompat.setAccessibilityDelegate(child, mChildAccessibilityDelegate); } } static boolean includeChildForAccessibility(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 AbsSavedState { int openDrawerGravity = Gravity.NO_GRAVITY; @LockMode int lockModeLeft; @LockMode int lockModeRight; @LockMode int lockModeStart; @LockMode int lockModeEnd; public SavedState(Parcel in, ClassLoader loader) { super(in, loader); openDrawerGravity = in.readInt(); lockModeLeft = in.readInt(); lockModeRight = in.readInt(); lockModeStart = in.readInt(); lockModeEnd = in.readInt(); } public SavedState(Parcelable superState) { super(superState); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(openDrawerGravity); dest.writeInt(lockModeLeft); dest.writeInt(lockModeRight); dest.writeInt(lockModeStart); dest.writeInt(lockModeEnd); } public static final Creator CREATOR = ParcelableCompat.newCreator( new ParcelableCompatCreatorCallbacks() { @Override public SavedState createFromParcel(Parcel in, ClassLoader loader) { return new SavedState(in, loader); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }); } private class ViewDragCallback extends ViewDragHelper.Callback { private final int mAbsGravity; private ViewDragHelper mDragger; private final Runnable mPeekRunnable = new Runnable() { @Override public void run() { peekDrawer(); } }; ViewDragCallback(int gravity) { mAbsGravity = gravity; } public void setDragger(ViewDragHelper dragger) { mDragger = dragger; } public void removeCallbacks() { DrawerLayout.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) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; } @Override public void onViewDragStateChanged(int state) { updateDrawerState(mAbsGravity, 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 (checkDrawerViewAbsoluteGravity(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 = mAbsGravity == 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 (checkDrawerViewAbsoluteGravity(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); } void peekDrawer() { final View toCapture; final int childLeft; final int peekDistance = mDragger.getEdgeSize(); final boolean leftEdge = mAbsGravity == 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(mAbsGravity); 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 isDrawerView(child) ? child.getWidth() : 0; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { if (checkDrawerViewAbsoluteGravity(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 { private static final int FLAG_IS_OPENED = 0x1; private static final int FLAG_IS_OPENING = 0x2; private static final int FLAG_IS_CLOSING = 0x4; public int gravity = Gravity.NO_GRAVITY; float onScreen; boolean isPeeking; int openState; 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) { if (CAN_HIDE_DESCENDANTS) { super.onInitializeAccessibilityNodeInfo(host, info); } else { // Obtain a node for the host, then manually generate the list // of children to only include non-obscured views. 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(); addChildrenForAccessibility(info, (ViewGroup) host); } info.setClassName(DrawerLayout.class.getName()); // This view reports itself as focusable so that it can intercept // the back button, but we should prevent this view from reporting // itself as focusable to accessibility services. info.setFocusable(false); info.setFocused(false); info.removeAction(AccessibilityActionCompat.ACTION_FOCUS); info.removeAction(AccessibilityActionCompat.ACTION_CLEAR_FOCUS); } @Override public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { super.onInitializeAccessibilityEvent(host, event); event.setClassName(DrawerLayout.class.getName()); } @Override public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { // Special case to handle window state change events. As far as // accessibility services are concerned, state changes from // DrawerLayout invalidate the entire contents of the screen (like // an Activity or Dialog) and they should announce the title of the // new content. if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { final List eventText = event.getText(); final View visibleDrawer = findVisibleDrawer(); if (visibleDrawer != null) { final int edgeGravity = getDrawerViewAbsoluteGravity(visibleDrawer); final CharSequence title = getDrawerTitle(edgeGravity); if (title != null) { eventText.add(title); } } return true; } return super.dispatchPopulateAccessibilityEvent(host, event); } @Override public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) { if (CAN_HIDE_DESCENDANTS || includeChildForAccessibility(child)) { return super.onRequestSendAccessibilityEvent(host, child, event); } return false; } private void addChildrenForAccessibility(AccessibilityNodeInfoCompat info, ViewGroup v) { final int childCount = v.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = v.getChildAt(i); if (includeChildForAccessibility(child)) { info.addChild(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()); } } final class ChildAccessibilityDelegate extends AccessibilityDelegateCompat { @Override public void onInitializeAccessibilityNodeInfo(View child, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(child, info); if (!includeChildForAccessibility(child)) { // If we are ignoring the sub-tree rooted at the child, // break the connection to the rest of the node tree. // For details refer to includeChildForAccessibility. info.setParent(null); } } } }