DrawerLayout.java revision 8a28dd16be442db2ee6275baf72079777627f37e
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18package android.support.v4.widget; 19 20import android.content.Context; 21import android.content.res.TypedArray; 22import android.graphics.Canvas; 23import android.graphics.Paint; 24import android.graphics.PixelFormat; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.os.Parcel; 28import android.os.Parcelable; 29import android.os.SystemClock; 30import android.support.annotation.DrawableRes; 31import android.support.annotation.IntDef; 32import android.support.annotation.Nullable; 33import android.support.v4.view.AccessibilityDelegateCompat; 34import android.support.v4.view.GravityCompat; 35import android.support.v4.view.KeyEventCompat; 36import android.support.v4.view.MotionEventCompat; 37import android.support.v4.view.ViewCompat; 38import android.support.v4.view.ViewGroupCompat; 39import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 40import android.util.AttributeSet; 41import android.view.Gravity; 42import android.view.KeyEvent; 43import android.view.MotionEvent; 44import android.view.View; 45import android.view.ViewGroup; 46import android.view.ViewGroup.LayoutParams; 47import android.view.ViewParent; 48import android.view.accessibility.AccessibilityEvent; 49 50import java.lang.annotation.Retention; 51import java.lang.annotation.RetentionPolicy; 52import java.util.List; 53 54/** 55 * DrawerLayout acts as a top-level container for window content that allows for 56 * interactive "drawer" views to be pulled out from the edge of the window. 57 * 58 * <p>Drawer positioning and layout is controlled using the <code>android:layout_gravity</code> 59 * attribute on child views corresponding to which side of the view you want the drawer 60 * to emerge from: left or right. (Or start/end on platform versions that support layout direction.) 61 * </p> 62 * 63 * <p>To use a DrawerLayout, position your primary content view as the first child with 64 * a width and height of <code>match_parent</code>. Add drawers as child views after the main 65 * content view and set the <code>layout_gravity</code> appropriately. Drawers commonly use 66 * <code>match_parent</code> for height with a fixed width.</p> 67 * 68 * <p>{@link DrawerListener} can be used to monitor the state and motion of drawer views. 69 * Avoid performing expensive operations such as layout during animation as it can cause 70 * stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state. 71 * {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.</p> 72 * 73 * <p>As per the <a href="{@docRoot}design/patterns/navigation-drawer.html">Android Design 74 * guide</a>, any drawers positioned to the left/start should 75 * always contain content for navigating around the application, whereas any drawers 76 * positioned to the right/end should always contain actions to take on the current content. 77 * This preserves the same navigation left, actions right structure present in the Action Bar 78 * and elsewhere.</p> 79 * 80 * <p>For more information about how to use DrawerLayout, read <a 81 * href="{@docRoot}training/implementing-navigation/nav-drawer.html">Creating a Navigation 82 * Drawer</a>.</p> 83 */ 84public class DrawerLayout extends ViewGroup { 85 private static final String TAG = "DrawerLayout"; 86 87 /** @hide */ 88 @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING}) 89 @Retention(RetentionPolicy.SOURCE) 90 private @interface State {} 91 92 /** 93 * Indicates that any drawers are in an idle, settled state. No animation is in progress. 94 */ 95 public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; 96 97 /** 98 * Indicates that a drawer is currently being dragged by the user. 99 */ 100 public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; 101 102 /** 103 * Indicates that a drawer is in the process of settling to a final position. 104 */ 105 public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; 106 107 /** @hide */ 108 @IntDef({LOCK_MODE_UNLOCKED, LOCK_MODE_LOCKED_CLOSED, LOCK_MODE_LOCKED_OPEN}) 109 @Retention(RetentionPolicy.SOURCE) 110 private @interface LockMode {} 111 112 /** 113 * The drawer is unlocked. 114 */ 115 public static final int LOCK_MODE_UNLOCKED = 0; 116 117 /** 118 * The drawer is locked closed. The user may not open it, though 119 * the app may open it programmatically. 120 */ 121 public static final int LOCK_MODE_LOCKED_CLOSED = 1; 122 123 /** 124 * The drawer is locked open. The user may not close it, though the app 125 * may close it programmatically. 126 */ 127 public static final int LOCK_MODE_LOCKED_OPEN = 2; 128 129 /** @hide */ 130 @IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END}) 131 @Retention(RetentionPolicy.SOURCE) 132 private @interface EdgeGravity {} 133 134 135 private static final int MIN_DRAWER_MARGIN = 64; // dp 136 137 private static final int DEFAULT_SCRIM_COLOR = 0x99000000; 138 139 /** 140 * Length of time to delay before peeking the drawer. 141 */ 142 private static final int PEEK_DELAY = 160; // ms 143 144 /** 145 * Minimum velocity that will be detected as a fling 146 */ 147 private static final int MIN_FLING_VELOCITY = 400; // dips per second 148 149 /** 150 * Experimental feature. 151 */ 152 private static final boolean ALLOW_EDGE_LOCK = false; 153 154 private static final boolean CHILDREN_DISALLOW_INTERCEPT = true; 155 156 private static final float TOUCH_SLOP_SENSITIVITY = 1.f; 157 158 private static final int[] LAYOUT_ATTRS = new int[] { 159 android.R.attr.layout_gravity 160 }; 161 162 private final ChildAccessibilityDelegate mChildAccessibilityDelegate = 163 new ChildAccessibilityDelegate(); 164 165 private int mMinDrawerMargin; 166 167 private int mScrimColor = DEFAULT_SCRIM_COLOR; 168 private float mScrimOpacity; 169 private Paint mScrimPaint = new Paint(); 170 171 private final ViewDragHelper mLeftDragger; 172 private final ViewDragHelper mRightDragger; 173 private final ViewDragCallback mLeftCallback; 174 private final ViewDragCallback mRightCallback; 175 private int mDrawerState; 176 private boolean mInLayout; 177 private boolean mFirstLayout = true; 178 private int mLockModeLeft; 179 private int mLockModeRight; 180 private boolean mDisallowInterceptRequested; 181 private boolean mChildrenCanceledTouch; 182 183 private DrawerListener mListener; 184 185 private float mInitialMotionX; 186 private float mInitialMotionY; 187 188 private Drawable mShadowLeft; 189 private Drawable mShadowRight; 190 191 private CharSequence mTitleLeft; 192 private CharSequence mTitleRight; 193 194 /** 195 * Listener for monitoring events about drawers. 196 */ 197 public interface DrawerListener { 198 /** 199 * Called when a drawer's position changes. 200 * @param drawerView The child view that was moved 201 * @param slideOffset The new offset of this drawer within its range, from 0-1 202 */ 203 public void onDrawerSlide(View drawerView, float slideOffset); 204 205 /** 206 * Called when a drawer has settled in a completely open state. 207 * The drawer is interactive at this point. 208 * 209 * @param drawerView Drawer view that is now open 210 */ 211 public void onDrawerOpened(View drawerView); 212 213 /** 214 * Called when a drawer has settled in a completely closed state. 215 * 216 * @param drawerView Drawer view that is now closed 217 */ 218 public void onDrawerClosed(View drawerView); 219 220 /** 221 * Called when the drawer motion state changes. The new state will 222 * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. 223 * 224 * @param newState The new drawer motion state 225 */ 226 public void onDrawerStateChanged(@State int newState); 227 } 228 229 /** 230 * Stub/no-op implementations of all methods of {@link DrawerListener}. 231 * Override this if you only care about a few of the available callback methods. 232 */ 233 public static abstract class SimpleDrawerListener implements DrawerListener { 234 @Override 235 public void onDrawerSlide(View drawerView, float slideOffset) { 236 } 237 238 @Override 239 public void onDrawerOpened(View drawerView) { 240 } 241 242 @Override 243 public void onDrawerClosed(View drawerView) { 244 } 245 246 @Override 247 public void onDrawerStateChanged(int newState) { 248 } 249 } 250 251 public DrawerLayout(Context context) { 252 this(context, null); 253 } 254 255 public DrawerLayout(Context context, AttributeSet attrs) { 256 this(context, attrs, 0); 257 } 258 259 public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { 260 super(context, attrs, defStyle); 261 262 final float density = getResources().getDisplayMetrics().density; 263 mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); 264 final float minVel = MIN_FLING_VELOCITY * density; 265 266 mLeftCallback = new ViewDragCallback(Gravity.LEFT); 267 mRightCallback = new ViewDragCallback(Gravity.RIGHT); 268 269 mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback); 270 mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); 271 mLeftDragger.setMinVelocity(minVel); 272 mLeftCallback.setDragger(mLeftDragger); 273 274 mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback); 275 mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); 276 mRightDragger.setMinVelocity(minVel); 277 mRightCallback.setDragger(mRightDragger); 278 279 // So that we can catch the back button 280 setFocusableInTouchMode(true); 281 282 ViewCompat.setImportantForAccessibility(this, 283 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 284 285 ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); 286 ViewGroupCompat.setMotionEventSplittingEnabled(this, false); 287 } 288 289 /** 290 * Set a simple drawable used for the left or right shadow. 291 * The drawable provided must have a nonzero intrinsic width. 292 * 293 * @param shadowDrawable Shadow drawable to use at the edge of a drawer 294 * @param gravity Which drawer the shadow should apply to 295 */ 296 public void setDrawerShadow(Drawable shadowDrawable, @EdgeGravity int gravity) { 297 /* 298 * TODO Someone someday might want to set more complex drawables here. 299 * They're probably nuts, but we might want to consider registering callbacks, 300 * setting states, etc. properly. 301 */ 302 303 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 304 ViewCompat.getLayoutDirection(this)); 305 if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { 306 mShadowLeft = shadowDrawable; 307 invalidate(); 308 } 309 if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { 310 mShadowRight = shadowDrawable; 311 invalidate(); 312 } 313 } 314 315 /** 316 * Set a simple drawable used for the left or right shadow. 317 * The drawable provided must have a nonzero intrinsic width. 318 * 319 * @param resId Resource id of a shadow drawable to use at the edge of a drawer 320 * @param gravity Which drawer the shadow should apply to 321 */ 322 public void setDrawerShadow(@DrawableRes int resId, @EdgeGravity int gravity) { 323 setDrawerShadow(getResources().getDrawable(resId), gravity); 324 } 325 326 /** 327 * Set a color to use for the scrim that obscures primary content while a drawer is open. 328 * 329 * @param color Color to use in 0xAARRGGBB format. 330 */ 331 public void setScrimColor(int color) { 332 mScrimColor = color; 333 invalidate(); 334 } 335 336 /** 337 * Set a listener to be notified of drawer events. 338 * 339 * @param listener Listener to notify when drawer events occur 340 * @see DrawerListener 341 */ 342 public void setDrawerListener(DrawerListener listener) { 343 mListener = listener; 344 } 345 346 /** 347 * Enable or disable interaction with all drawers. 348 * 349 * <p>This allows the application to restrict the user's ability to open or close 350 * any drawer within this layout. DrawerLayout will still respond to calls to 351 * {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.</p> 352 * 353 * <p>Locking drawers open or closed will implicitly open or close 354 * any drawers as appropriate.</p> 355 * 356 * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, 357 * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. 358 */ 359 public void setDrawerLockMode(@LockMode int lockMode) { 360 setDrawerLockMode(lockMode, Gravity.LEFT); 361 setDrawerLockMode(lockMode, Gravity.RIGHT); 362 } 363 364 /** 365 * Enable or disable interaction with the given drawer. 366 * 367 * <p>This allows the application to restrict the user's ability to open or close 368 * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, 369 * {@link #closeDrawer(int)} and friends if a drawer is locked.</p> 370 * 371 * <p>Locking a drawer open or closed will implicitly open or close 372 * that drawer as appropriate.</p> 373 * 374 * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, 375 * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. 376 * @param edgeGravity Gravity.LEFT, RIGHT, START or END. 377 * Expresses which drawer to change the mode for. 378 * 379 * @see #LOCK_MODE_UNLOCKED 380 * @see #LOCK_MODE_LOCKED_CLOSED 381 * @see #LOCK_MODE_LOCKED_OPEN 382 */ 383 public void setDrawerLockMode(@LockMode int lockMode, @EdgeGravity int edgeGravity) { 384 final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity, 385 ViewCompat.getLayoutDirection(this)); 386 if (absGravity == Gravity.LEFT) { 387 mLockModeLeft = lockMode; 388 } else if (absGravity == Gravity.RIGHT) { 389 mLockModeRight = lockMode; 390 } 391 if (lockMode != LOCK_MODE_UNLOCKED) { 392 // Cancel interaction in progress 393 final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger; 394 helper.cancel(); 395 } 396 switch (lockMode) { 397 case LOCK_MODE_LOCKED_OPEN: 398 final View toOpen = findDrawerWithGravity(absGravity); 399 if (toOpen != null) { 400 openDrawer(toOpen); 401 } 402 break; 403 case LOCK_MODE_LOCKED_CLOSED: 404 final View toClose = findDrawerWithGravity(absGravity); 405 if (toClose != null) { 406 closeDrawer(toClose); 407 } 408 break; 409 // default: do nothing 410 } 411 } 412 413 /** 414 * Enable or disable interaction with the given drawer. 415 * 416 * <p>This allows the application to restrict the user's ability to open or close 417 * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, 418 * {@link #closeDrawer(int)} and friends if a drawer is locked.</p> 419 * 420 * <p>Locking a drawer open or closed will implicitly open or close 421 * that drawer as appropriate.</p> 422 * 423 * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, 424 * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. 425 * @param drawerView The drawer view to change the lock mode for 426 * 427 * @see #LOCK_MODE_UNLOCKED 428 * @see #LOCK_MODE_LOCKED_CLOSED 429 * @see #LOCK_MODE_LOCKED_OPEN 430 */ 431 public void setDrawerLockMode(@LockMode int lockMode, View drawerView) { 432 if (!isDrawerView(drawerView)) { 433 throw new IllegalArgumentException("View " + drawerView + " is not a " + 434 "drawer with appropriate layout_gravity"); 435 } 436 final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; 437 setDrawerLockMode(lockMode, gravity); 438 } 439 440 /** 441 * Check the lock mode of the drawer with the given gravity. 442 * 443 * @param edgeGravity Gravity of the drawer to check 444 * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or 445 * {@link #LOCK_MODE_LOCKED_OPEN}. 446 */ 447 @LockMode 448 public int getDrawerLockMode(@EdgeGravity int edgeGravity) { 449 final int absGravity = GravityCompat.getAbsoluteGravity( 450 edgeGravity, ViewCompat.getLayoutDirection(this)); 451 if (absGravity == Gravity.LEFT) { 452 return mLockModeLeft; 453 } else if (absGravity == Gravity.RIGHT) { 454 return mLockModeRight; 455 } 456 return LOCK_MODE_UNLOCKED; 457 } 458 459 /** 460 * Check the lock mode of the given drawer view. 461 * 462 * @param drawerView Drawer view to check lock mode 463 * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or 464 * {@link #LOCK_MODE_LOCKED_OPEN}. 465 */ 466 @LockMode 467 public int getDrawerLockMode(View drawerView) { 468 final int absGravity = getDrawerViewAbsoluteGravity(drawerView); 469 if (absGravity == Gravity.LEFT) { 470 return mLockModeLeft; 471 } else if (absGravity == Gravity.RIGHT) { 472 return mLockModeRight; 473 } 474 return LOCK_MODE_UNLOCKED; 475 } 476 477 /** 478 * Sets the title of the drawer with the given gravity. 479 * <p> 480 * When accessibility is turned on, this is the title that will be used to 481 * identify the drawer to the active accessibility service. 482 * 483 * @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which 484 * drawer to set the title for. 485 * @param title The title for the drawer. 486 */ 487 public void setDrawerTitle(@EdgeGravity int edgeGravity, CharSequence title) { 488 final int absGravity = GravityCompat.getAbsoluteGravity( 489 edgeGravity, ViewCompat.getLayoutDirection(this)); 490 if (absGravity == Gravity.LEFT) { 491 mTitleLeft = title; 492 } else if (absGravity == Gravity.RIGHT) { 493 mTitleRight = title; 494 } 495 } 496 497 /** 498 * Returns the title of the drawer with the given gravity. 499 * 500 * @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which 501 * drawer to return the title for. 502 * @return The title of the drawer, or null if none set. 503 * @see #setDrawerTitle(int, CharSequence) 504 */ 505 @Nullable 506 public CharSequence getDrawerTitle(@EdgeGravity int edgeGravity) { 507 final int absGravity = GravityCompat.getAbsoluteGravity( 508 edgeGravity, ViewCompat.getLayoutDirection(this)); 509 if (absGravity == Gravity.LEFT) { 510 return mTitleLeft; 511 } else if (absGravity == Gravity.RIGHT) { 512 return mTitleRight; 513 } 514 return null; 515 } 516 517 /** 518 * Resolve the shared state of all drawers from the component ViewDragHelpers. 519 * Should be called whenever a ViewDragHelper's state changes. 520 */ 521 void updateDrawerState(int forGravity, @State int activeState, View activeDrawer) { 522 final int leftState = mLeftDragger.getViewDragState(); 523 final int rightState = mRightDragger.getViewDragState(); 524 525 final int state; 526 if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) { 527 state = STATE_DRAGGING; 528 } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) { 529 state = STATE_SETTLING; 530 } else { 531 state = STATE_IDLE; 532 } 533 534 if (activeDrawer != null && activeState == STATE_IDLE) { 535 final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams(); 536 if (lp.onScreen == 0) { 537 dispatchOnDrawerClosed(activeDrawer); 538 } else if (lp.onScreen == 1) { 539 dispatchOnDrawerOpened(activeDrawer); 540 } 541 } 542 543 if (state != mDrawerState) { 544 mDrawerState = state; 545 546 if (mListener != null) { 547 mListener.onDrawerStateChanged(state); 548 } 549 } 550 } 551 552 void dispatchOnDrawerClosed(View drawerView) { 553 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 554 if (lp.knownOpen) { 555 lp.knownOpen = false; 556 if (mListener != null) { 557 mListener.onDrawerClosed(drawerView); 558 } 559 560 // If no drawer is opened, all drawers are not shown 561 // for accessibility and the content is shown. 562 View content = getChildAt(0); 563 if (content != null) { 564 ViewCompat.setImportantForAccessibility(content, 565 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 566 } 567 ViewCompat.setImportantForAccessibility(drawerView, 568 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 569 570 // Only send WINDOW_STATE_CHANGE if the host has window focus. This 571 // may change if support for multiple foreground windows (e.g. IME) 572 // improves. 573 if (hasWindowFocus()) { 574 final View rootView = getRootView(); 575 if (rootView != null) { 576 rootView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 577 } 578 } 579 } 580 } 581 582 void dispatchOnDrawerOpened(View drawerView) { 583 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 584 if (!lp.knownOpen) { 585 lp.knownOpen = true; 586 if (mListener != null) { 587 mListener.onDrawerOpened(drawerView); 588 } 589 590 // If a drawer is opened, only it is shown for 591 // accessibility and the content is not shown. 592 View content = getChildAt(0); 593 if (content != null) { 594 ViewCompat.setImportantForAccessibility(content, 595 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 596 } 597 ViewCompat.setImportantForAccessibility(drawerView, 598 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 599 600 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 601 drawerView.requestFocus(); 602 } 603 } 604 605 void dispatchOnDrawerSlide(View drawerView, float slideOffset) { 606 if (mListener != null) { 607 mListener.onDrawerSlide(drawerView, slideOffset); 608 } 609 } 610 611 void setDrawerViewOffset(View drawerView, float slideOffset) { 612 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 613 if (slideOffset == lp.onScreen) { 614 return; 615 } 616 617 lp.onScreen = slideOffset; 618 dispatchOnDrawerSlide(drawerView, slideOffset); 619 } 620 621 float getDrawerViewOffset(View drawerView) { 622 return ((LayoutParams) drawerView.getLayoutParams()).onScreen; 623 } 624 625 /** 626 * @return the absolute gravity of the child drawerView, resolved according 627 * to the current layout direction 628 */ 629 int getDrawerViewAbsoluteGravity(View drawerView) { 630 final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; 631 return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); 632 } 633 634 boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) { 635 final int absGravity = getDrawerViewAbsoluteGravity(drawerView); 636 return (absGravity & checkFor) == checkFor; 637 } 638 639 View findOpenDrawer() { 640 final int childCount = getChildCount(); 641 for (int i = 0; i < childCount; i++) { 642 final View child = getChildAt(i); 643 if (((LayoutParams) child.getLayoutParams()).knownOpen) { 644 return child; 645 } 646 } 647 return null; 648 } 649 650 void moveDrawerToOffset(View drawerView, float slideOffset) { 651 final float oldOffset = getDrawerViewOffset(drawerView); 652 final int width = drawerView.getWidth(); 653 final int oldPos = (int) (width * oldOffset); 654 final int newPos = (int) (width * slideOffset); 655 final int dx = newPos - oldPos; 656 657 drawerView.offsetLeftAndRight( 658 checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx); 659 setDrawerViewOffset(drawerView, slideOffset); 660 } 661 662 /** 663 * @param gravity the gravity of the child to return. If specified as a 664 * relative value, it will be resolved according to the current 665 * layout direction. 666 * @return the drawer with the specified gravity 667 */ 668 View findDrawerWithGravity(int gravity) { 669 final int absHorizGravity = GravityCompat.getAbsoluteGravity( 670 gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK; 671 final int childCount = getChildCount(); 672 for (int i = 0; i < childCount; i++) { 673 final View child = getChildAt(i); 674 final int childAbsGravity = getDrawerViewAbsoluteGravity(child); 675 if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) { 676 return child; 677 } 678 } 679 return null; 680 } 681 682 /** 683 * Simple gravity to string - only supports LEFT and RIGHT for debugging output. 684 * 685 * @param gravity Absolute gravity value 686 * @return LEFT or RIGHT as appropriate, or a hex string 687 */ 688 static String gravityToString(@EdgeGravity int gravity) { 689 if ((gravity & Gravity.LEFT) == Gravity.LEFT) { 690 return "LEFT"; 691 } 692 if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { 693 return "RIGHT"; 694 } 695 return Integer.toHexString(gravity); 696 } 697 698 @Override 699 protected void onDetachedFromWindow() { 700 super.onDetachedFromWindow(); 701 mFirstLayout = true; 702 } 703 704 @Override 705 protected void onAttachedToWindow() { 706 super.onAttachedToWindow(); 707 mFirstLayout = true; 708 } 709 710 @Override 711 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 712 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 713 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 714 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 715 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 716 717 if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { 718 if (isInEditMode()) { 719 // Don't crash the layout editor. Consume all of the space if specified 720 // or pick a magic number from thin air otherwise. 721 // TODO Better communication with tools of this bogus state. 722 // It will crash on a real device. 723 if (widthMode == MeasureSpec.AT_MOST) { 724 widthMode = MeasureSpec.EXACTLY; 725 } else if (widthMode == MeasureSpec.UNSPECIFIED) { 726 widthMode = MeasureSpec.EXACTLY; 727 widthSize = 300; 728 } 729 if (heightMode == MeasureSpec.AT_MOST) { 730 heightMode = MeasureSpec.EXACTLY; 731 } 732 else if (heightMode == MeasureSpec.UNSPECIFIED) { 733 heightMode = MeasureSpec.EXACTLY; 734 heightSize = 300; 735 } 736 } else { 737 throw new IllegalArgumentException( 738 "DrawerLayout must be measured with MeasureSpec.EXACTLY."); 739 } 740 } 741 742 setMeasuredDimension(widthSize, heightSize); 743 744 // Gravity value for each drawer we've seen. Only one of each permitted. 745 int foundDrawers = 0; 746 final int childCount = getChildCount(); 747 for (int i = 0; i < childCount; i++) { 748 final View child = getChildAt(i); 749 750 if (child.getVisibility() == GONE) { 751 continue; 752 } 753 754 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 755 756 if (isContentView(child)) { 757 // Content views get measured at exactly the layout's size. 758 final int contentWidthSpec = MeasureSpec.makeMeasureSpec( 759 widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); 760 final int contentHeightSpec = MeasureSpec.makeMeasureSpec( 761 heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); 762 child.measure(contentWidthSpec, contentHeightSpec); 763 } else if (isDrawerView(child)) { 764 final int childGravity = 765 getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; 766 if ((foundDrawers & childGravity) != 0) { 767 throw new IllegalStateException("Child drawer has absolute gravity " + 768 gravityToString(childGravity) + " but this " + TAG + " already has a " + 769 "drawer view along that edge"); 770 } 771 final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, 772 mMinDrawerMargin + lp.leftMargin + lp.rightMargin, 773 lp.width); 774 final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, 775 lp.topMargin + lp.bottomMargin, 776 lp.height); 777 child.measure(drawerWidthSpec, drawerHeightSpec); 778 } else { 779 throw new IllegalStateException("Child " + child + " at index " + i + 780 " does not have a valid layout_gravity - must be Gravity.LEFT, " + 781 "Gravity.RIGHT or Gravity.NO_GRAVITY"); 782 } 783 } 784 } 785 786 @Override 787 protected void onLayout(boolean changed, int l, int t, int r, int b) { 788 mInLayout = true; 789 final int width = r - l; 790 final int childCount = getChildCount(); 791 for (int i = 0; i < childCount; i++) { 792 final View child = getChildAt(i); 793 794 if (child.getVisibility() == GONE) { 795 continue; 796 } 797 798 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 799 800 if (isContentView(child)) { 801 child.layout(lp.leftMargin, lp.topMargin, 802 lp.leftMargin + child.getMeasuredWidth(), 803 lp.topMargin + child.getMeasuredHeight()); 804 } else { // Drawer, if it wasn't onMeasure would have thrown an exception. 805 final int childWidth = child.getMeasuredWidth(); 806 final int childHeight = child.getMeasuredHeight(); 807 int childLeft; 808 809 final float newOffset; 810 if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 811 childLeft = -childWidth + (int) (childWidth * lp.onScreen); 812 newOffset = (float) (childWidth + childLeft) / childWidth; 813 } else { // Right; onMeasure checked for us. 814 childLeft = width - (int) (childWidth * lp.onScreen); 815 newOffset = (float) (width - childLeft) / childWidth; 816 } 817 818 final boolean changeOffset = newOffset != lp.onScreen; 819 820 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 821 822 switch (vgrav) { 823 default: 824 case Gravity.TOP: { 825 child.layout(childLeft, lp.topMargin, childLeft + childWidth, 826 lp.topMargin + childHeight); 827 break; 828 } 829 830 case Gravity.BOTTOM: { 831 final int height = b - t; 832 child.layout(childLeft, 833 height - lp.bottomMargin - child.getMeasuredHeight(), 834 childLeft + childWidth, 835 height - lp.bottomMargin); 836 break; 837 } 838 839 case Gravity.CENTER_VERTICAL: { 840 final int height = b - t; 841 int childTop = (height - childHeight) / 2; 842 843 // Offset for margins. If things don't fit right because of 844 // bad measurement before, oh well. 845 if (childTop < lp.topMargin) { 846 childTop = lp.topMargin; 847 } else if (childTop + childHeight > height - lp.bottomMargin) { 848 childTop = height - lp.bottomMargin - childHeight; 849 } 850 child.layout(childLeft, childTop, childLeft + childWidth, 851 childTop + childHeight); 852 break; 853 } 854 } 855 856 if (changeOffset) { 857 setDrawerViewOffset(child, newOffset); 858 } 859 860 final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; 861 if (child.getVisibility() != newVisibility) { 862 child.setVisibility(newVisibility); 863 } 864 } 865 } 866 mInLayout = false; 867 mFirstLayout = false; 868 } 869 870 @Override 871 public void requestLayout() { 872 if (!mInLayout) { 873 super.requestLayout(); 874 } 875 } 876 877 @Override 878 public void computeScroll() { 879 final int childCount = getChildCount(); 880 float scrimOpacity = 0; 881 for (int i = 0; i < childCount; i++) { 882 final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; 883 scrimOpacity = Math.max(scrimOpacity, onscreen); 884 } 885 mScrimOpacity = scrimOpacity; 886 887 // "|" used on purpose; both need to run. 888 if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { 889 ViewCompat.postInvalidateOnAnimation(this); 890 } 891 } 892 893 private static boolean hasOpaqueBackground(View v) { 894 final Drawable bg = v.getBackground(); 895 if (bg != null) { 896 return bg.getOpacity() == PixelFormat.OPAQUE; 897 } 898 return false; 899 } 900 901 @Override 902 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 903 final int height = getHeight(); 904 final boolean drawingContent = isContentView(child); 905 int clipLeft = 0, clipRight = getWidth(); 906 907 final int restoreCount = canvas.save(); 908 if (drawingContent) { 909 final int childCount = getChildCount(); 910 for (int i = 0; i < childCount; i++) { 911 final View v = getChildAt(i); 912 if (v == child || v.getVisibility() != VISIBLE || 913 !hasOpaqueBackground(v) || !isDrawerView(v) || 914 v.getHeight() < height) { 915 continue; 916 } 917 918 if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) { 919 final int vright = v.getRight(); 920 if (vright > clipLeft) clipLeft = vright; 921 } else { 922 final int vleft = v.getLeft(); 923 if (vleft < clipRight) clipRight = vleft; 924 } 925 } 926 canvas.clipRect(clipLeft, 0, clipRight, getHeight()); 927 } 928 final boolean result = super.drawChild(canvas, child, drawingTime); 929 canvas.restoreToCount(restoreCount); 930 931 if (mScrimOpacity > 0 && drawingContent) { 932 final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; 933 final int imag = (int) (baseAlpha * mScrimOpacity); 934 final int color = imag << 24 | (mScrimColor & 0xffffff); 935 mScrimPaint.setColor(color); 936 937 canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); 938 } else if (mShadowLeft != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 939 final int shadowWidth = mShadowLeft.getIntrinsicWidth(); 940 final int childRight = child.getRight(); 941 final int drawerPeekDistance = mLeftDragger.getEdgeSize(); 942 final float alpha = 943 Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); 944 mShadowLeft.setBounds(childRight, child.getTop(), 945 childRight + shadowWidth, child.getBottom()); 946 mShadowLeft.setAlpha((int) (0xff * alpha)); 947 mShadowLeft.draw(canvas); 948 } else if (mShadowRight != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { 949 final int shadowWidth = mShadowRight.getIntrinsicWidth(); 950 final int childLeft = child.getLeft(); 951 final int showing = getWidth() - childLeft; 952 final int drawerPeekDistance = mRightDragger.getEdgeSize(); 953 final float alpha = 954 Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); 955 mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), 956 childLeft, child.getBottom()); 957 mShadowRight.setAlpha((int) (0xff * alpha)); 958 mShadowRight.draw(canvas); 959 } 960 return result; 961 } 962 963 boolean isContentView(View child) { 964 return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; 965 } 966 967 boolean isDrawerView(View child) { 968 final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; 969 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 970 ViewCompat.getLayoutDirection(child)); 971 return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; 972 } 973 974 @Override 975 public boolean onInterceptTouchEvent(MotionEvent ev) { 976 final int action = MotionEventCompat.getActionMasked(ev); 977 978 // "|" used deliberately here; both methods should be invoked. 979 final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | 980 mRightDragger.shouldInterceptTouchEvent(ev); 981 982 boolean interceptForTap = false; 983 984 switch (action) { 985 case MotionEvent.ACTION_DOWN: { 986 final float x = ev.getX(); 987 final float y = ev.getY(); 988 mInitialMotionX = x; 989 mInitialMotionY = y; 990 if (mScrimOpacity > 0 && 991 isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) { 992 interceptForTap = true; 993 } 994 mDisallowInterceptRequested = false; 995 mChildrenCanceledTouch = false; 996 break; 997 } 998 999 case MotionEvent.ACTION_MOVE: { 1000 // If we cross the touch slop, don't perform the delayed peek for an edge touch. 1001 if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { 1002 mLeftCallback.removeCallbacks(); 1003 mRightCallback.removeCallbacks(); 1004 } 1005 break; 1006 } 1007 1008 case MotionEvent.ACTION_CANCEL: 1009 case MotionEvent.ACTION_UP: { 1010 closeDrawers(true); 1011 mDisallowInterceptRequested = false; 1012 mChildrenCanceledTouch = false; 1013 } 1014 } 1015 1016 return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; 1017 } 1018 1019 @Override 1020 public boolean onTouchEvent(MotionEvent ev) { 1021 mLeftDragger.processTouchEvent(ev); 1022 mRightDragger.processTouchEvent(ev); 1023 1024 final int action = ev.getAction(); 1025 boolean wantTouchEvents = true; 1026 1027 switch (action & MotionEventCompat.ACTION_MASK) { 1028 case MotionEvent.ACTION_DOWN: { 1029 final float x = ev.getX(); 1030 final float y = ev.getY(); 1031 mInitialMotionX = x; 1032 mInitialMotionY = y; 1033 mDisallowInterceptRequested = false; 1034 mChildrenCanceledTouch = false; 1035 break; 1036 } 1037 1038 case MotionEvent.ACTION_UP: { 1039 final float x = ev.getX(); 1040 final float y = ev.getY(); 1041 boolean peekingOnly = true; 1042 final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y); 1043 if (touchedView != null && isContentView(touchedView)) { 1044 final float dx = x - mInitialMotionX; 1045 final float dy = y - mInitialMotionY; 1046 final int slop = mLeftDragger.getTouchSlop(); 1047 if (dx * dx + dy * dy < slop * slop) { 1048 // Taps close a dimmed open drawer but only if it isn't locked open. 1049 final View openDrawer = findOpenDrawer(); 1050 if (openDrawer != null) { 1051 peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN; 1052 } 1053 } 1054 } 1055 closeDrawers(peekingOnly); 1056 mDisallowInterceptRequested = false; 1057 break; 1058 } 1059 1060 case MotionEvent.ACTION_CANCEL: { 1061 closeDrawers(true); 1062 mDisallowInterceptRequested = false; 1063 mChildrenCanceledTouch = false; 1064 break; 1065 } 1066 } 1067 1068 return wantTouchEvents; 1069 } 1070 1071 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 1072 if (CHILDREN_DISALLOW_INTERCEPT || 1073 (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && 1074 !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT))) { 1075 // If we have an edge touch we want to skip this and track it for later instead. 1076 super.requestDisallowInterceptTouchEvent(disallowIntercept); 1077 } 1078 mDisallowInterceptRequested = disallowIntercept; 1079 if (disallowIntercept) { 1080 closeDrawers(true); 1081 } 1082 } 1083 1084 /** 1085 * Close all currently open drawer views by animating them out of view. 1086 */ 1087 public void closeDrawers() { 1088 closeDrawers(false); 1089 } 1090 1091 void closeDrawers(boolean peekingOnly) { 1092 boolean needsInvalidate = false; 1093 final int childCount = getChildCount(); 1094 for (int i = 0; i < childCount; i++) { 1095 final View child = getChildAt(i); 1096 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1097 1098 if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { 1099 continue; 1100 } 1101 1102 final int childWidth = child.getWidth(); 1103 1104 if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 1105 needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, 1106 -childWidth, child.getTop()); 1107 } else { 1108 needsInvalidate |= mRightDragger.smoothSlideViewTo(child, 1109 getWidth(), child.getTop()); 1110 } 1111 1112 lp.isPeeking = false; 1113 } 1114 1115 mLeftCallback.removeCallbacks(); 1116 mRightCallback.removeCallbacks(); 1117 1118 if (needsInvalidate) { 1119 invalidate(); 1120 } 1121 } 1122 1123 /** 1124 * Open the specified drawer view by animating it into view. 1125 * 1126 * @param drawerView Drawer view to open 1127 */ 1128 public void openDrawer(View drawerView) { 1129 if (!isDrawerView(drawerView)) { 1130 throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); 1131 } 1132 1133 if (mFirstLayout) { 1134 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 1135 lp.onScreen = 1.f; 1136 lp.knownOpen = true; 1137 } else { 1138 if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { 1139 mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); 1140 } else { 1141 mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), 1142 drawerView.getTop()); 1143 } 1144 } 1145 invalidate(); 1146 } 1147 1148 /** 1149 * Open the specified drawer by animating it out of view. 1150 * 1151 * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. 1152 * GravityCompat.START or GravityCompat.END may also be used. 1153 */ 1154 public void openDrawer(@EdgeGravity int gravity) { 1155 final View drawerView = findDrawerWithGravity(gravity); 1156 if (drawerView == null) { 1157 throw new IllegalArgumentException("No drawer view found with gravity " + 1158 gravityToString(gravity)); 1159 } 1160 openDrawer(drawerView); 1161 } 1162 1163 /** 1164 * Close the specified drawer view by animating it into view. 1165 * 1166 * @param drawerView Drawer view to close 1167 */ 1168 public void closeDrawer(View drawerView) { 1169 if (!isDrawerView(drawerView)) { 1170 throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); 1171 } 1172 1173 if (mFirstLayout) { 1174 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 1175 lp.onScreen = 0.f; 1176 lp.knownOpen = false; 1177 } else { 1178 if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { 1179 mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), 1180 drawerView.getTop()); 1181 } else { 1182 mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); 1183 } 1184 } 1185 invalidate(); 1186 } 1187 1188 /** 1189 * Close the specified drawer by animating it out of view. 1190 * 1191 * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. 1192 * GravityCompat.START or GravityCompat.END may also be used. 1193 */ 1194 public void closeDrawer(@EdgeGravity int gravity) { 1195 final View drawerView = findDrawerWithGravity(gravity); 1196 if (drawerView == null) { 1197 throw new IllegalArgumentException("No drawer view found with gravity " + 1198 gravityToString(gravity)); 1199 } 1200 closeDrawer(drawerView); 1201 } 1202 1203 /** 1204 * Check if the given drawer view is currently in an open state. 1205 * To be considered "open" the drawer must have settled into its fully 1206 * visible state. To check for partial visibility use 1207 * {@link #isDrawerVisible(android.view.View)}. 1208 * 1209 * @param drawer Drawer view to check 1210 * @return true if the given drawer view is in an open state 1211 * @see #isDrawerVisible(android.view.View) 1212 */ 1213 public boolean isDrawerOpen(View drawer) { 1214 if (!isDrawerView(drawer)) { 1215 throw new IllegalArgumentException("View " + drawer + " is not a drawer"); 1216 } 1217 return ((LayoutParams) drawer.getLayoutParams()).knownOpen; 1218 } 1219 1220 /** 1221 * Check if the given drawer view is currently in an open state. 1222 * To be considered "open" the drawer must have settled into its fully 1223 * visible state. If there is no drawer with the given gravity this method 1224 * will return false. 1225 * 1226 * @param drawerGravity Gravity of the drawer to check 1227 * @return true if the given drawer view is in an open state 1228 */ 1229 public boolean isDrawerOpen(@EdgeGravity int drawerGravity) { 1230 final View drawerView = findDrawerWithGravity(drawerGravity); 1231 if (drawerView != null) { 1232 return isDrawerOpen(drawerView); 1233 } 1234 return false; 1235 } 1236 1237 /** 1238 * Check if a given drawer view is currently visible on-screen. The drawer 1239 * may be only peeking onto the screen, fully extended, or anywhere inbetween. 1240 * 1241 * @param drawer Drawer view to check 1242 * @return true if the given drawer is visible on-screen 1243 * @see #isDrawerOpen(android.view.View) 1244 */ 1245 public boolean isDrawerVisible(View drawer) { 1246 if (!isDrawerView(drawer)) { 1247 throw new IllegalArgumentException("View " + drawer + " is not a drawer"); 1248 } 1249 return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; 1250 } 1251 1252 /** 1253 * Check if a given drawer view is currently visible on-screen. The drawer 1254 * may be only peeking onto the screen, fully extended, or anywhere in between. 1255 * If there is no drawer with the given gravity this method will return false. 1256 * 1257 * @param drawerGravity Gravity of the drawer to check 1258 * @return true if the given drawer is visible on-screen 1259 */ 1260 public boolean isDrawerVisible(@EdgeGravity int drawerGravity) { 1261 final View drawerView = findDrawerWithGravity(drawerGravity); 1262 if (drawerView != null) { 1263 return isDrawerVisible(drawerView); 1264 } 1265 return false; 1266 } 1267 1268 private boolean hasPeekingDrawer() { 1269 final int childCount = getChildCount(); 1270 for (int i = 0; i < childCount; i++) { 1271 final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 1272 if (lp.isPeeking) { 1273 return true; 1274 } 1275 } 1276 return false; 1277 } 1278 1279 @Override 1280 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1281 return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); 1282 } 1283 1284 @Override 1285 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1286 return p instanceof LayoutParams 1287 ? new LayoutParams((LayoutParams) p) 1288 : p instanceof ViewGroup.MarginLayoutParams 1289 ? new LayoutParams((MarginLayoutParams) p) 1290 : new LayoutParams(p); 1291 } 1292 1293 @Override 1294 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1295 return p instanceof LayoutParams && super.checkLayoutParams(p); 1296 } 1297 1298 @Override 1299 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1300 return new LayoutParams(getContext(), attrs); 1301 } 1302 1303 private boolean hasVisibleDrawer() { 1304 return findVisibleDrawer() != null; 1305 } 1306 1307 private View findVisibleDrawer() { 1308 final int childCount = getChildCount(); 1309 for (int i = 0; i < childCount; i++) { 1310 final View child = getChildAt(i); 1311 if (isDrawerView(child) && isDrawerVisible(child)) { 1312 return child; 1313 } 1314 } 1315 return null; 1316 } 1317 1318 void cancelChildViewTouch() { 1319 // Cancel child touches 1320 if (!mChildrenCanceledTouch) { 1321 final long now = SystemClock.uptimeMillis(); 1322 final MotionEvent cancelEvent = MotionEvent.obtain(now, now, 1323 MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); 1324 final int childCount = getChildCount(); 1325 for (int i = 0; i < childCount; i++) { 1326 getChildAt(i).dispatchTouchEvent(cancelEvent); 1327 } 1328 cancelEvent.recycle(); 1329 mChildrenCanceledTouch = true; 1330 } 1331 } 1332 1333 @Override 1334 public boolean onKeyDown(int keyCode, KeyEvent event) { 1335 if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { 1336 KeyEventCompat.startTracking(event); 1337 return true; 1338 } 1339 return super.onKeyDown(keyCode, event); 1340 } 1341 1342 @Override 1343 public boolean onKeyUp(int keyCode, KeyEvent event) { 1344 if (keyCode == KeyEvent.KEYCODE_BACK) { 1345 final View visibleDrawer = findVisibleDrawer(); 1346 if (visibleDrawer != null && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED) { 1347 closeDrawers(); 1348 } 1349 return visibleDrawer != null; 1350 } 1351 return super.onKeyUp(keyCode, event); 1352 } 1353 1354 @Override 1355 protected void onRestoreInstanceState(Parcelable state) { 1356 final SavedState ss = (SavedState) state; 1357 super.onRestoreInstanceState(ss.getSuperState()); 1358 1359 if (ss.openDrawerGravity != Gravity.NO_GRAVITY) { 1360 final View toOpen = findDrawerWithGravity(ss.openDrawerGravity); 1361 if (toOpen != null) { 1362 openDrawer(toOpen); 1363 } 1364 } 1365 1366 setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); 1367 setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); 1368 } 1369 1370 @Override 1371 protected Parcelable onSaveInstanceState() { 1372 final Parcelable superState = super.onSaveInstanceState(); 1373 1374 final SavedState ss = new SavedState(superState); 1375 1376 final int childCount = getChildCount(); 1377 for (int i = 0; i < childCount; i++) { 1378 final View child = getChildAt(i); 1379 if (!isDrawerView(child)) { 1380 continue; 1381 } 1382 1383 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1384 if (lp.knownOpen) { 1385 ss.openDrawerGravity = lp.gravity; 1386 // Only one drawer can be open at a time. 1387 break; 1388 } 1389 } 1390 1391 ss.lockModeLeft = mLockModeLeft; 1392 ss.lockModeRight = mLockModeRight; 1393 1394 return ss; 1395 } 1396 1397 @Override 1398 public void addView(View child, int index, ViewGroup.LayoutParams params) { 1399 // Until a drawer is open, it is hidden from accessibility. 1400 if (index > 0 || (index < 0 && getChildCount() > 0)) { 1401 ViewCompat.setImportantForAccessibility(child, 1402 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); 1403 // Also set a delegate to break the child-parent relation if the 1404 // child is hidden. For details (see incluceChildForAccessibility). 1405 ViewCompat.setAccessibilityDelegate(child, mChildAccessibilityDelegate); 1406 } else { 1407 // Initially, the content is shown for accessibility. 1408 ViewCompat.setImportantForAccessibility(child, 1409 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 1410 } 1411 super.addView(child, index, params); 1412 } 1413 1414 private static boolean includeChildForAccessibilitiy(View child) { 1415 // If the child is not important for accessibility we make 1416 // sure this hides the entire subtree rooted at it as the 1417 // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDATS is not 1418 // supported on older platforms but we want to hide the entire 1419 // content and not opened drawers if a drawer is opened. 1420 return ViewCompat.getImportantForAccessibility(child) 1421 != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS 1422 && ViewCompat.getImportantForAccessibility(child) 1423 != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO; 1424 } 1425 1426 /** 1427 * State persisted across instances 1428 */ 1429 protected static class SavedState extends BaseSavedState { 1430 int openDrawerGravity = Gravity.NO_GRAVITY; 1431 int lockModeLeft = LOCK_MODE_UNLOCKED; 1432 int lockModeRight = LOCK_MODE_UNLOCKED; 1433 1434 public SavedState(Parcel in) { 1435 super(in); 1436 openDrawerGravity = in.readInt(); 1437 } 1438 1439 public SavedState(Parcelable superState) { 1440 super(superState); 1441 } 1442 1443 @Override 1444 public void writeToParcel(Parcel dest, int flags) { 1445 super.writeToParcel(dest, flags); 1446 dest.writeInt(openDrawerGravity); 1447 } 1448 1449 public static final Parcelable.Creator<SavedState> CREATOR = 1450 new Parcelable.Creator<SavedState>() { 1451 @Override 1452 public SavedState createFromParcel(Parcel source) { 1453 return new SavedState(source); 1454 } 1455 1456 @Override 1457 public SavedState[] newArray(int size) { 1458 return new SavedState[size]; 1459 } 1460 }; 1461 } 1462 1463 private class ViewDragCallback extends ViewDragHelper.Callback { 1464 private final int mAbsGravity; 1465 private ViewDragHelper mDragger; 1466 1467 private final Runnable mPeekRunnable = new Runnable() { 1468 @Override public void run() { 1469 peekDrawer(); 1470 } 1471 }; 1472 1473 public ViewDragCallback(int gravity) { 1474 mAbsGravity = gravity; 1475 } 1476 1477 public void setDragger(ViewDragHelper dragger) { 1478 mDragger = dragger; 1479 } 1480 1481 public void removeCallbacks() { 1482 DrawerLayout.this.removeCallbacks(mPeekRunnable); 1483 } 1484 1485 @Override 1486 public boolean tryCaptureView(View child, int pointerId) { 1487 // Only capture views where the gravity matches what we're looking for. 1488 // This lets us use two ViewDragHelpers, one for each side drawer. 1489 return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) 1490 && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; 1491 } 1492 1493 @Override 1494 public void onViewDragStateChanged(int state) { 1495 updateDrawerState(mAbsGravity, state, mDragger.getCapturedView()); 1496 } 1497 1498 @Override 1499 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 1500 float offset; 1501 final int childWidth = changedView.getWidth(); 1502 1503 // This reverses the positioning shown in onLayout. 1504 if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) { 1505 offset = (float) (childWidth + left) / childWidth; 1506 } else { 1507 final int width = getWidth(); 1508 offset = (float) (width - left) / childWidth; 1509 } 1510 setDrawerViewOffset(changedView, offset); 1511 changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); 1512 invalidate(); 1513 } 1514 1515 @Override 1516 public void onViewCaptured(View capturedChild, int activePointerId) { 1517 final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); 1518 lp.isPeeking = false; 1519 1520 closeOtherDrawer(); 1521 } 1522 1523 private void closeOtherDrawer() { 1524 final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; 1525 final View toClose = findDrawerWithGravity(otherGrav); 1526 if (toClose != null) { 1527 closeDrawer(toClose); 1528 } 1529 } 1530 1531 @Override 1532 public void onViewReleased(View releasedChild, float xvel, float yvel) { 1533 // Offset is how open the drawer is, therefore left/right values 1534 // are reversed from one another. 1535 final float offset = getDrawerViewOffset(releasedChild); 1536 final int childWidth = releasedChild.getWidth(); 1537 1538 int left; 1539 if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) { 1540 left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; 1541 } else { 1542 final int width = getWidth(); 1543 left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width; 1544 } 1545 1546 mDragger.settleCapturedViewAt(left, releasedChild.getTop()); 1547 invalidate(); 1548 } 1549 1550 @Override 1551 public void onEdgeTouched(int edgeFlags, int pointerId) { 1552 postDelayed(mPeekRunnable, PEEK_DELAY); 1553 } 1554 1555 private void peekDrawer() { 1556 final View toCapture; 1557 final int childLeft; 1558 final int peekDistance = mDragger.getEdgeSize(); 1559 final boolean leftEdge = mAbsGravity == Gravity.LEFT; 1560 if (leftEdge) { 1561 toCapture = findDrawerWithGravity(Gravity.LEFT); 1562 childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; 1563 } else { 1564 toCapture = findDrawerWithGravity(Gravity.RIGHT); 1565 childLeft = getWidth() - peekDistance; 1566 } 1567 // Only peek if it would mean making the drawer more visible and the drawer isn't locked 1568 if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || 1569 (!leftEdge && toCapture.getLeft() > childLeft)) && 1570 getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { 1571 final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); 1572 mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); 1573 lp.isPeeking = true; 1574 invalidate(); 1575 1576 closeOtherDrawer(); 1577 1578 cancelChildViewTouch(); 1579 } 1580 } 1581 1582 @Override 1583 public boolean onEdgeLock(int edgeFlags) { 1584 if (ALLOW_EDGE_LOCK) { 1585 final View drawer = findDrawerWithGravity(mAbsGravity); 1586 if (drawer != null && !isDrawerOpen(drawer)) { 1587 closeDrawer(drawer); 1588 } 1589 return true; 1590 } 1591 return false; 1592 } 1593 1594 @Override 1595 public void onEdgeDragStarted(int edgeFlags, int pointerId) { 1596 final View toCapture; 1597 if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { 1598 toCapture = findDrawerWithGravity(Gravity.LEFT); 1599 } else { 1600 toCapture = findDrawerWithGravity(Gravity.RIGHT); 1601 } 1602 1603 if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { 1604 mDragger.captureChildView(toCapture, pointerId); 1605 } 1606 } 1607 1608 @Override 1609 public int getViewHorizontalDragRange(View child) { 1610 return isDrawerView(child) ? child.getWidth() : 0; 1611 } 1612 1613 @Override 1614 public int clampViewPositionHorizontal(View child, int left, int dx) { 1615 if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 1616 return Math.max(-child.getWidth(), Math.min(left, 0)); 1617 } else { 1618 final int width = getWidth(); 1619 return Math.max(width - child.getWidth(), Math.min(left, width)); 1620 } 1621 } 1622 1623 @Override 1624 public int clampViewPositionVertical(View child, int top, int dy) { 1625 return child.getTop(); 1626 } 1627 } 1628 1629 public static class LayoutParams extends ViewGroup.MarginLayoutParams { 1630 1631 public int gravity = Gravity.NO_GRAVITY; 1632 float onScreen; 1633 boolean isPeeking; 1634 boolean knownOpen; 1635 1636 public LayoutParams(Context c, AttributeSet attrs) { 1637 super(c, attrs); 1638 1639 final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 1640 this.gravity = a.getInt(0, Gravity.NO_GRAVITY); 1641 a.recycle(); 1642 } 1643 1644 public LayoutParams(int width, int height) { 1645 super(width, height); 1646 } 1647 1648 public LayoutParams(int width, int height, int gravity) { 1649 this(width, height); 1650 this.gravity = gravity; 1651 } 1652 1653 public LayoutParams(LayoutParams source) { 1654 super(source); 1655 this.gravity = source.gravity; 1656 } 1657 1658 public LayoutParams(ViewGroup.LayoutParams source) { 1659 super(source); 1660 } 1661 1662 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1663 super(source); 1664 } 1665 } 1666 1667 class AccessibilityDelegate extends AccessibilityDelegateCompat { 1668 private final Rect mTmpRect = new Rect(); 1669 1670 @Override 1671 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 1672 final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); 1673 super.onInitializeAccessibilityNodeInfo(host, superNode); 1674 1675 info.setClassName(DrawerLayout.class.getName()); 1676 info.setSource(host); 1677 final ViewParent parent = ViewCompat.getParentForAccessibility(host); 1678 if (parent instanceof View) { 1679 info.setParent((View) parent); 1680 } 1681 copyNodeInfoNoChildren(info, superNode); 1682 1683 superNode.recycle(); 1684 1685 addChildrenForAccessibility(info, (ViewGroup) host); 1686 } 1687 1688 @Override 1689 public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 1690 super.onInitializeAccessibilityEvent(host, event); 1691 1692 event.setClassName(DrawerLayout.class.getName()); 1693 } 1694 1695 @Override 1696 public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 1697 // Special case to handle window state change events. As far as 1698 // accessibility services are concerned, state changes from 1699 // DrawerLayout invalidate the entire contents of the screen (like 1700 // an Activity or Dialog) and they should announce the title of the 1701 // new content. 1702 if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 1703 final List<CharSequence> eventText = event.getText(); 1704 final View visibleDrawer = findVisibleDrawer(); 1705 if (visibleDrawer != null) { 1706 final int edgeGravity = getDrawerViewAbsoluteGravity(visibleDrawer); 1707 final CharSequence title = getDrawerTitle(edgeGravity); 1708 if (title != null) { 1709 eventText.add(title); 1710 } 1711 } 1712 1713 return true; 1714 } 1715 1716 return super.dispatchPopulateAccessibilityEvent(host, event); 1717 } 1718 1719 private void addChildrenForAccessibility(AccessibilityNodeInfoCompat info, ViewGroup v) { 1720 final int childCount = v.getChildCount(); 1721 for (int i = 0; i < childCount; i++) { 1722 final View child = v.getChildAt(i); 1723 if (includeChildForAccessibilitiy(child)) { 1724 info.addChild(child); 1725 } 1726 } 1727 } 1728 1729 @Override 1730 public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, 1731 AccessibilityEvent event) { 1732 if (includeChildForAccessibilitiy(child)) { 1733 return super.onRequestSendAccessibilityEvent(host, child, event); 1734 } 1735 return false; 1736 } 1737 1738 /** 1739 * This should really be in AccessibilityNodeInfoCompat, but there unfortunately 1740 * seem to be a few elements that are not easily cloneable using the underlying API. 1741 * Leave it private here as it's not general-purpose useful. 1742 */ 1743 private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, 1744 AccessibilityNodeInfoCompat src) { 1745 final Rect rect = mTmpRect; 1746 1747 src.getBoundsInParent(rect); 1748 dest.setBoundsInParent(rect); 1749 1750 src.getBoundsInScreen(rect); 1751 dest.setBoundsInScreen(rect); 1752 1753 dest.setVisibleToUser(src.isVisibleToUser()); 1754 dest.setPackageName(src.getPackageName()); 1755 dest.setClassName(src.getClassName()); 1756 dest.setContentDescription(src.getContentDescription()); 1757 1758 dest.setEnabled(src.isEnabled()); 1759 dest.setClickable(src.isClickable()); 1760 dest.setFocusable(src.isFocusable()); 1761 dest.setFocused(src.isFocused()); 1762 dest.setAccessibilityFocused(src.isAccessibilityFocused()); 1763 dest.setSelected(src.isSelected()); 1764 dest.setLongClickable(src.isLongClickable()); 1765 1766 dest.addAction(src.getActions()); 1767 } 1768 } 1769 1770 final class ChildAccessibilityDelegate extends AccessibilityDelegateCompat { 1771 @Override 1772 public void onInitializeAccessibilityNodeInfo(View child, 1773 AccessibilityNodeInfoCompat info) { 1774 super.onInitializeAccessibilityNodeInfo(child, info); 1775 if (!includeChildForAccessibilitiy(child)) { 1776 // If we are ignoring the sub-tree rooted at the child, 1777 // break the connection to the rest of the node tree. 1778 // For details refer to includeChildForAccessibilitiy. 1779 info.setParent(null); 1780 } 1781 } 1782 } 1783} 1784