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