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