DrawerLayout.java revision d6edbe712f21ea4924ab9fb49ebd8c94d1e03884
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 absGravity = GravityCompat.getAbsoluteGravity(edgeGravity, 347 ViewCompat.getLayoutDirection(this)); 348 if (absGravity == Gravity.LEFT) { 349 mLockModeLeft = lockMode; 350 } else if (absGravity == Gravity.RIGHT) { 351 mLockModeRight = lockMode; 352 } 353 if (lockMode != LOCK_MODE_UNLOCKED) { 354 // Cancel interaction in progress 355 final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger; 356 helper.cancel(); 357 } 358 switch (lockMode) { 359 case LOCK_MODE_LOCKED_OPEN: 360 final View toOpen = findDrawerWithGravity(absGravity); 361 if (toOpen != null) { 362 openDrawer(toOpen); 363 } 364 break; 365 case LOCK_MODE_LOCKED_CLOSED: 366 final View toClose = findDrawerWithGravity(absGravity); 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 final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; 399 setDrawerLockMode(lockMode, gravity); 400 } 401 402 /** 403 * Check the lock mode of the drawer with the given gravity. 404 * 405 * @param edgeGravity Gravity of the drawer to check 406 * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or 407 * {@link #LOCK_MODE_LOCKED_OPEN}. 408 */ 409 public int getDrawerLockMode(int edgeGravity) { 410 final int absGravity = GravityCompat.getAbsoluteGravity( 411 edgeGravity, ViewCompat.getLayoutDirection(this)); 412 if (absGravity == Gravity.LEFT) { 413 return mLockModeLeft; 414 } else if (absGravity == Gravity.RIGHT) { 415 return mLockModeRight; 416 } 417 return LOCK_MODE_UNLOCKED; 418 } 419 420 /** 421 * Check the lock mode of the given drawer view. 422 * 423 * @param drawerView Drawer view to check lock mode 424 * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or 425 * {@link #LOCK_MODE_LOCKED_OPEN}. 426 */ 427 public int getDrawerLockMode(View drawerView) { 428 final int absGravity = getDrawerViewAbsoluteGravity(drawerView); 429 if (absGravity == Gravity.LEFT) { 430 return mLockModeLeft; 431 } else if (absGravity == Gravity.RIGHT) { 432 return mLockModeRight; 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 /** 515 * @return the absolute gravity of the child drawerView, resolved according 516 * to the current layout direction 517 */ 518 int getDrawerViewAbsoluteGravity(View drawerView) { 519 final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; 520 return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); 521 } 522 523 boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) { 524 final int absGravity = getDrawerViewAbsoluteGravity(drawerView); 525 return (absGravity & checkFor) == checkFor; 526 } 527 528 View findOpenDrawer() { 529 final int childCount = getChildCount(); 530 for (int i = 0; i < childCount; i++) { 531 final View child = getChildAt(i); 532 if (((LayoutParams) child.getLayoutParams()).knownOpen) { 533 return child; 534 } 535 } 536 return null; 537 } 538 539 void moveDrawerToOffset(View drawerView, float slideOffset) { 540 final float oldOffset = getDrawerViewOffset(drawerView); 541 final int width = drawerView.getWidth(); 542 final int oldPos = (int) (width * oldOffset); 543 final int newPos = (int) (width * slideOffset); 544 final int dx = newPos - oldPos; 545 546 drawerView.offsetLeftAndRight( 547 checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx); 548 setDrawerViewOffset(drawerView, slideOffset); 549 } 550 551 /** 552 * @param gravity the gravity of the child to return. If specified as a 553 * relative value, it will be resolved according to the current 554 * layout direction. 555 * @return the drawer with the specified gravity 556 */ 557 View findDrawerWithGravity(int gravity) { 558 final int absHorizGravity = GravityCompat.getAbsoluteGravity( 559 gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK; 560 final int childCount = getChildCount(); 561 for (int i = 0; i < childCount; i++) { 562 final View child = getChildAt(i); 563 final int childAbsGravity = getDrawerViewAbsoluteGravity(child); 564 if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) { 565 return child; 566 } 567 } 568 return null; 569 } 570 571 /** 572 * Simple gravity to string - only supports LEFT and RIGHT for debugging output. 573 * 574 * @param gravity Absolute gravity value 575 * @return LEFT or RIGHT as appropriate, or a hex string 576 */ 577 static String gravityToString(int gravity) { 578 if ((gravity & Gravity.LEFT) == Gravity.LEFT) { 579 return "LEFT"; 580 } 581 if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { 582 return "RIGHT"; 583 } 584 return Integer.toHexString(gravity); 585 } 586 587 @Override 588 protected void onDetachedFromWindow() { 589 super.onDetachedFromWindow(); 590 mFirstLayout = true; 591 } 592 593 @Override 594 protected void onAttachedToWindow() { 595 super.onAttachedToWindow(); 596 mFirstLayout = true; 597 } 598 599 @Override 600 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 601 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 602 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 603 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 604 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 605 606 if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { 607 if (isInEditMode()) { 608 // Don't crash the layout editor. Consume all of the space if specified 609 // or pick a magic number from thin air otherwise. 610 // TODO Better communication with tools of this bogus state. 611 // It will crash on a real device. 612 if (widthMode == MeasureSpec.AT_MOST) { 613 widthMode = MeasureSpec.EXACTLY; 614 } else if (widthMode == MeasureSpec.UNSPECIFIED) { 615 widthMode = MeasureSpec.EXACTLY; 616 widthSize = 300; 617 } 618 if (heightMode == MeasureSpec.AT_MOST) { 619 heightMode = MeasureSpec.EXACTLY; 620 } 621 else if (heightMode == MeasureSpec.UNSPECIFIED) { 622 heightMode = MeasureSpec.EXACTLY; 623 heightSize = 300; 624 } 625 } else { 626 throw new IllegalArgumentException( 627 "DrawerLayout must be measured with MeasureSpec.EXACTLY."); 628 } 629 } 630 631 setMeasuredDimension(widthSize, heightSize); 632 633 // Gravity value for each drawer we've seen. Only one of each permitted. 634 int foundDrawers = 0; 635 final int childCount = getChildCount(); 636 for (int i = 0; i < childCount; i++) { 637 final View child = getChildAt(i); 638 639 if (child.getVisibility() == GONE) { 640 continue; 641 } 642 643 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 644 645 if (isContentView(child)) { 646 // Content views get measured at exactly the layout's size. 647 final int contentWidthSpec = MeasureSpec.makeMeasureSpec( 648 widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); 649 final int contentHeightSpec = MeasureSpec.makeMeasureSpec( 650 heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); 651 child.measure(contentWidthSpec, contentHeightSpec); 652 } else if (isDrawerView(child)) { 653 final int childGravity = 654 getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; 655 if ((foundDrawers & childGravity) != 0) { 656 throw new IllegalStateException("Child drawer has absolute gravity " + 657 gravityToString(childGravity) + " but this " + TAG + " already has a " + 658 "drawer view along that edge"); 659 } 660 final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, 661 mMinDrawerMargin + lp.leftMargin + lp.rightMargin, 662 lp.width); 663 final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, 664 lp.topMargin + lp.bottomMargin, 665 lp.height); 666 child.measure(drawerWidthSpec, drawerHeightSpec); 667 } else { 668 throw new IllegalStateException("Child " + child + " at index " + i + 669 " does not have a valid layout_gravity - must be Gravity.LEFT, " + 670 "Gravity.RIGHT or Gravity.NO_GRAVITY"); 671 } 672 } 673 } 674 675 @Override 676 protected void onLayout(boolean changed, int l, int t, int r, int b) { 677 mInLayout = true; 678 final int width = r - l; 679 final int childCount = getChildCount(); 680 for (int i = 0; i < childCount; i++) { 681 final View child = getChildAt(i); 682 683 if (child.getVisibility() == GONE) { 684 continue; 685 } 686 687 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 688 689 if (isContentView(child)) { 690 child.layout(lp.leftMargin, lp.topMargin, 691 lp.leftMargin + child.getMeasuredWidth(), 692 lp.topMargin + child.getMeasuredHeight()); 693 } else { // Drawer, if it wasn't onMeasure would have thrown an exception. 694 final int childWidth = child.getMeasuredWidth(); 695 final int childHeight = child.getMeasuredHeight(); 696 int childLeft; 697 698 final float newOffset; 699 if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 700 childLeft = -childWidth + (int) (childWidth * lp.onScreen); 701 newOffset = (float) (childWidth + childLeft) / childWidth; 702 } else { // Right; onMeasure checked for us. 703 childLeft = width - (int) (childWidth * lp.onScreen); 704 newOffset = (float) (width - childLeft) / childWidth; 705 } 706 707 final boolean changeOffset = newOffset != lp.onScreen; 708 709 final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; 710 711 switch (vgrav) { 712 default: 713 case Gravity.TOP: { 714 child.layout(childLeft, lp.topMargin, childLeft + childWidth, 715 lp.topMargin + childHeight); 716 break; 717 } 718 719 case Gravity.BOTTOM: { 720 final int height = b - t; 721 child.layout(childLeft, 722 height - lp.bottomMargin - child.getMeasuredHeight(), 723 childLeft + childWidth, 724 height - lp.bottomMargin); 725 break; 726 } 727 728 case Gravity.CENTER_VERTICAL: { 729 final int height = b - t; 730 int childTop = (height - childHeight) / 2; 731 732 // Offset for margins. If things don't fit right because of 733 // bad measurement before, oh well. 734 if (childTop < lp.topMargin) { 735 childTop = lp.topMargin; 736 } else if (childTop + childHeight > height - lp.bottomMargin) { 737 childTop = height - lp.bottomMargin - childHeight; 738 } 739 child.layout(childLeft, childTop, childLeft + childWidth, 740 childTop + childHeight); 741 break; 742 } 743 } 744 745 if (changeOffset) { 746 setDrawerViewOffset(child, newOffset); 747 } 748 749 final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; 750 if (child.getVisibility() != newVisibility) { 751 child.setVisibility(newVisibility); 752 } 753 } 754 } 755 mInLayout = false; 756 mFirstLayout = false; 757 } 758 759 @Override 760 public void requestLayout() { 761 if (!mInLayout) { 762 super.requestLayout(); 763 } 764 } 765 766 @Override 767 public void computeScroll() { 768 final int childCount = getChildCount(); 769 float scrimOpacity = 0; 770 for (int i = 0; i < childCount; i++) { 771 final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; 772 scrimOpacity = Math.max(scrimOpacity, onscreen); 773 } 774 mScrimOpacity = scrimOpacity; 775 776 // "|" used on purpose; both need to run. 777 if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { 778 ViewCompat.postInvalidateOnAnimation(this); 779 } 780 } 781 782 private static boolean hasOpaqueBackground(View v) { 783 final Drawable bg = v.getBackground(); 784 if (bg != null) { 785 return bg.getOpacity() == PixelFormat.OPAQUE; 786 } 787 return false; 788 } 789 790 @Override 791 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 792 final int height = getHeight(); 793 final boolean drawingContent = isContentView(child); 794 int clipLeft = 0, clipRight = getWidth(); 795 796 final int restoreCount = canvas.save(); 797 if (drawingContent) { 798 final int childCount = getChildCount(); 799 for (int i = 0; i < childCount; i++) { 800 final View v = getChildAt(i); 801 if (v == child || v.getVisibility() != VISIBLE || 802 !hasOpaqueBackground(v) || !isDrawerView(v) || 803 v.getHeight() < height) { 804 continue; 805 } 806 807 if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) { 808 final int vright = v.getRight(); 809 if (vright > clipLeft) clipLeft = vright; 810 } else { 811 final int vleft = v.getLeft(); 812 if (vleft < clipRight) clipRight = vleft; 813 } 814 } 815 canvas.clipRect(clipLeft, 0, clipRight, getHeight()); 816 } 817 final boolean result = super.drawChild(canvas, child, drawingTime); 818 canvas.restoreToCount(restoreCount); 819 820 if (mScrimOpacity > 0 && drawingContent) { 821 final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; 822 final int imag = (int) (baseAlpha * mScrimOpacity); 823 final int color = imag << 24 | (mScrimColor & 0xffffff); 824 mScrimPaint.setColor(color); 825 826 canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); 827 } else if (mShadowLeft != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 828 final int shadowWidth = mShadowLeft.getIntrinsicWidth(); 829 final int childRight = child.getRight(); 830 final int drawerPeekDistance = mLeftDragger.getEdgeSize(); 831 final float alpha = 832 Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); 833 mShadowLeft.setBounds(childRight, child.getTop(), 834 childRight + shadowWidth, child.getBottom()); 835 mShadowLeft.setAlpha((int) (0xff * alpha)); 836 mShadowLeft.draw(canvas); 837 } else if (mShadowRight != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { 838 final int shadowWidth = mShadowRight.getIntrinsicWidth(); 839 final int childLeft = child.getLeft(); 840 final int showing = getWidth() - childLeft; 841 final int drawerPeekDistance = mRightDragger.getEdgeSize(); 842 final float alpha = 843 Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); 844 mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), 845 childLeft, child.getBottom()); 846 mShadowRight.setAlpha((int) (0xff * alpha)); 847 mShadowRight.draw(canvas); 848 } 849 return result; 850 } 851 852 boolean isContentView(View child) { 853 return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; 854 } 855 856 boolean isDrawerView(View child) { 857 final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; 858 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 859 ViewCompat.getLayoutDirection(child)); 860 return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; 861 } 862 863 @Override 864 public boolean onInterceptTouchEvent(MotionEvent ev) { 865 final int action = MotionEventCompat.getActionMasked(ev); 866 867 // "|" used deliberately here; both methods should be invoked. 868 final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | 869 mRightDragger.shouldInterceptTouchEvent(ev); 870 871 boolean interceptForTap = false; 872 873 switch (action) { 874 case MotionEvent.ACTION_DOWN: { 875 final float x = ev.getX(); 876 final float y = ev.getY(); 877 mInitialMotionX = x; 878 mInitialMotionY = y; 879 if (mScrimOpacity > 0 && 880 isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) { 881 interceptForTap = true; 882 } 883 mDisallowInterceptRequested = false; 884 mChildrenCanceledTouch = false; 885 break; 886 } 887 888 case MotionEvent.ACTION_MOVE: { 889 // If we cross the touch slop, don't perform the delayed peek for an edge touch. 890 if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { 891 mLeftCallback.removeCallbacks(); 892 mRightCallback.removeCallbacks(); 893 } 894 break; 895 } 896 897 case MotionEvent.ACTION_CANCEL: 898 case MotionEvent.ACTION_UP: { 899 closeDrawers(true); 900 mDisallowInterceptRequested = false; 901 mChildrenCanceledTouch = false; 902 } 903 } 904 905 return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; 906 } 907 908 @Override 909 public boolean onTouchEvent(MotionEvent ev) { 910 mLeftDragger.processTouchEvent(ev); 911 mRightDragger.processTouchEvent(ev); 912 913 final int action = ev.getAction(); 914 boolean wantTouchEvents = true; 915 916 switch (action & MotionEventCompat.ACTION_MASK) { 917 case MotionEvent.ACTION_DOWN: { 918 final float x = ev.getX(); 919 final float y = ev.getY(); 920 mInitialMotionX = x; 921 mInitialMotionY = y; 922 mDisallowInterceptRequested = false; 923 mChildrenCanceledTouch = false; 924 break; 925 } 926 927 case MotionEvent.ACTION_UP: { 928 final float x = ev.getX(); 929 final float y = ev.getY(); 930 boolean peekingOnly = true; 931 final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y); 932 if (touchedView != null && isContentView(touchedView)) { 933 final float dx = x - mInitialMotionX; 934 final float dy = y - mInitialMotionY; 935 final int slop = mLeftDragger.getTouchSlop(); 936 if (dx * dx + dy * dy < slop * slop) { 937 // Taps close a dimmed open drawer but only if it isn't locked open. 938 final View openDrawer = findOpenDrawer(); 939 if (openDrawer != null) { 940 peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN; 941 } 942 } 943 } 944 closeDrawers(peekingOnly); 945 mDisallowInterceptRequested = false; 946 break; 947 } 948 949 case MotionEvent.ACTION_CANCEL: { 950 closeDrawers(true); 951 mDisallowInterceptRequested = false; 952 mChildrenCanceledTouch = false; 953 break; 954 } 955 } 956 957 return wantTouchEvents; 958 } 959 960 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 961 if (CHILDREN_DISALLOW_INTERCEPT || 962 (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && 963 !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT))) { 964 // If we have an edge touch we want to skip this and track it for later instead. 965 super.requestDisallowInterceptTouchEvent(disallowIntercept); 966 } 967 mDisallowInterceptRequested = disallowIntercept; 968 if (disallowIntercept) { 969 closeDrawers(true); 970 } 971 } 972 973 /** 974 * Close all currently open drawer views by animating them out of view. 975 */ 976 public void closeDrawers() { 977 closeDrawers(false); 978 } 979 980 void closeDrawers(boolean peekingOnly) { 981 boolean needsInvalidate = false; 982 final int childCount = getChildCount(); 983 for (int i = 0; i < childCount; i++) { 984 final View child = getChildAt(i); 985 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 986 987 if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { 988 continue; 989 } 990 991 final int childWidth = child.getWidth(); 992 993 if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 994 needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, 995 -childWidth, child.getTop()); 996 } else { 997 needsInvalidate |= mRightDragger.smoothSlideViewTo(child, 998 getWidth(), child.getTop()); 999 } 1000 1001 lp.isPeeking = false; 1002 } 1003 1004 mLeftCallback.removeCallbacks(); 1005 mRightCallback.removeCallbacks(); 1006 1007 if (needsInvalidate) { 1008 invalidate(); 1009 } 1010 } 1011 1012 /** 1013 * Open the specified drawer view by animating it into view. 1014 * 1015 * @param drawerView Drawer view to open 1016 */ 1017 public void openDrawer(View drawerView) { 1018 if (!isDrawerView(drawerView)) { 1019 throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); 1020 } 1021 1022 if (mFirstLayout) { 1023 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 1024 lp.onScreen = 1.f; 1025 lp.knownOpen = true; 1026 } else { 1027 if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { 1028 mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); 1029 } else { 1030 mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), 1031 drawerView.getTop()); 1032 } 1033 } 1034 invalidate(); 1035 } 1036 1037 /** 1038 * Open the specified drawer by animating it out of view. 1039 * 1040 * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. 1041 * GravityCompat.START or GravityCompat.END may also be used. 1042 */ 1043 public void openDrawer(int gravity) { 1044 final View drawerView = findDrawerWithGravity(gravity); 1045 if (drawerView == null) { 1046 throw new IllegalArgumentException("No drawer view found with gravity " + 1047 gravityToString(gravity)); 1048 } 1049 openDrawer(drawerView); 1050 } 1051 1052 /** 1053 * Close the specified drawer view by animating it into view. 1054 * 1055 * @param drawerView Drawer view to close 1056 */ 1057 public void closeDrawer(View drawerView) { 1058 if (!isDrawerView(drawerView)) { 1059 throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); 1060 } 1061 1062 if (mFirstLayout) { 1063 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 1064 lp.onScreen = 0.f; 1065 lp.knownOpen = false; 1066 } else { 1067 if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { 1068 mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), 1069 drawerView.getTop()); 1070 } else { 1071 mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); 1072 } 1073 } 1074 invalidate(); 1075 } 1076 1077 /** 1078 * Close the specified drawer by animating it out of view. 1079 * 1080 * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. 1081 * GravityCompat.START or GravityCompat.END may also be used. 1082 */ 1083 public void closeDrawer(int gravity) { 1084 final View drawerView = findDrawerWithGravity(gravity); 1085 if (drawerView == null) { 1086 throw new IllegalArgumentException("No drawer view found with gravity " + 1087 gravityToString(gravity)); 1088 } 1089 closeDrawer(drawerView); 1090 } 1091 1092 /** 1093 * Check if the given drawer view is currently in an open state. 1094 * To be considered "open" the drawer must have settled into its fully 1095 * visible state. To check for partial visibility use 1096 * {@link #isDrawerVisible(android.view.View)}. 1097 * 1098 * @param drawer Drawer view to check 1099 * @return true if the given drawer view is in an open state 1100 * @see #isDrawerVisible(android.view.View) 1101 */ 1102 public boolean isDrawerOpen(View drawer) { 1103 if (!isDrawerView(drawer)) { 1104 throw new IllegalArgumentException("View " + drawer + " is not a drawer"); 1105 } 1106 return ((LayoutParams) drawer.getLayoutParams()).knownOpen; 1107 } 1108 1109 /** 1110 * Check if the given drawer view is currently in an open state. 1111 * To be considered "open" the drawer must have settled into its fully 1112 * visible state. If there is no drawer with the given gravity this method 1113 * will return false. 1114 * 1115 * @param drawerGravity Gravity of the drawer to check 1116 * @return true if the given drawer view is in an open state 1117 */ 1118 public boolean isDrawerOpen(int drawerGravity) { 1119 final View drawerView = findDrawerWithGravity(drawerGravity); 1120 if (drawerView != null) { 1121 return isDrawerOpen(drawerView); 1122 } 1123 return false; 1124 } 1125 1126 /** 1127 * Check if a given drawer view is currently visible on-screen. The drawer 1128 * may be only peeking onto the screen, fully extended, or anywhere inbetween. 1129 * 1130 * @param drawer Drawer view to check 1131 * @return true if the given drawer is visible on-screen 1132 * @see #isDrawerOpen(android.view.View) 1133 */ 1134 public boolean isDrawerVisible(View drawer) { 1135 if (!isDrawerView(drawer)) { 1136 throw new IllegalArgumentException("View " + drawer + " is not a drawer"); 1137 } 1138 return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; 1139 } 1140 1141 /** 1142 * Check if a given drawer view is currently visible on-screen. The drawer 1143 * may be only peeking onto the screen, fully extended, or anywhere inbetween. 1144 * If there is no drawer with the given gravity this method will return false. 1145 * 1146 * @param drawerGravity Gravity of the drawer to check 1147 * @return true if the given drawer is visible on-screen 1148 */ 1149 public boolean isDrawerVisible(int drawerGravity) { 1150 final View drawerView = findDrawerWithGravity(drawerGravity); 1151 if (drawerView != null) { 1152 return isDrawerVisible(drawerView); 1153 } 1154 return false; 1155 } 1156 1157 private boolean hasPeekingDrawer() { 1158 final int childCount = getChildCount(); 1159 for (int i = 0; i < childCount; i++) { 1160 final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 1161 if (lp.isPeeking) { 1162 return true; 1163 } 1164 } 1165 return false; 1166 } 1167 1168 @Override 1169 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1170 return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); 1171 } 1172 1173 @Override 1174 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1175 return p instanceof LayoutParams 1176 ? new LayoutParams((LayoutParams) p) 1177 : p instanceof ViewGroup.MarginLayoutParams 1178 ? new LayoutParams((MarginLayoutParams) p) 1179 : new LayoutParams(p); 1180 } 1181 1182 @Override 1183 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1184 return p instanceof LayoutParams && super.checkLayoutParams(p); 1185 } 1186 1187 @Override 1188 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1189 return new LayoutParams(getContext(), attrs); 1190 } 1191 1192 private boolean hasVisibleDrawer() { 1193 return findVisibleDrawer() != null; 1194 } 1195 1196 private View findVisibleDrawer() { 1197 final int childCount = getChildCount(); 1198 for (int i = 0; i < childCount; i++) { 1199 final View child = getChildAt(i); 1200 if (isDrawerView(child) && isDrawerVisible(child)) { 1201 return child; 1202 } 1203 } 1204 return null; 1205 } 1206 1207 void cancelChildViewTouch() { 1208 // Cancel child touches 1209 if (!mChildrenCanceledTouch) { 1210 final long now = SystemClock.uptimeMillis(); 1211 final MotionEvent cancelEvent = MotionEvent.obtain(now, now, 1212 MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); 1213 final int childCount = getChildCount(); 1214 for (int i = 0; i < childCount; i++) { 1215 getChildAt(i).dispatchTouchEvent(cancelEvent); 1216 } 1217 cancelEvent.recycle(); 1218 mChildrenCanceledTouch = true; 1219 } 1220 } 1221 1222 @Override 1223 public boolean onKeyDown(int keyCode, KeyEvent event) { 1224 if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { 1225 KeyEventCompat.startTracking(event); 1226 return true; 1227 } 1228 return super.onKeyDown(keyCode, event); 1229 } 1230 1231 @Override 1232 public boolean onKeyUp(int keyCode, KeyEvent event) { 1233 if (keyCode == KeyEvent.KEYCODE_BACK) { 1234 final View visibleDrawer = findVisibleDrawer(); 1235 if (visibleDrawer != null && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED) { 1236 closeDrawers(); 1237 } 1238 return visibleDrawer != null; 1239 } 1240 return super.onKeyUp(keyCode, event); 1241 } 1242 1243 @Override 1244 protected void onRestoreInstanceState(Parcelable state) { 1245 final SavedState ss = (SavedState) state; 1246 super.onRestoreInstanceState(ss.getSuperState()); 1247 1248 if (ss.openDrawerGravity != Gravity.NO_GRAVITY) { 1249 final View toOpen = findDrawerWithGravity(ss.openDrawerGravity); 1250 if (toOpen != null) { 1251 openDrawer(toOpen); 1252 } 1253 } 1254 1255 setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); 1256 setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); 1257 } 1258 1259 @Override 1260 protected Parcelable onSaveInstanceState() { 1261 final Parcelable superState = super.onSaveInstanceState(); 1262 1263 final SavedState ss = new SavedState(superState); 1264 1265 final int childCount = getChildCount(); 1266 for (int i = 0; i < childCount; i++) { 1267 final View child = getChildAt(i); 1268 if (!isDrawerView(child)) { 1269 continue; 1270 } 1271 1272 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1273 if (lp.knownOpen) { 1274 ss.openDrawerGravity = lp.gravity; 1275 // Only one drawer can be open at a time. 1276 break; 1277 } 1278 } 1279 1280 ss.lockModeLeft = mLockModeLeft; 1281 ss.lockModeRight = mLockModeRight; 1282 1283 return ss; 1284 } 1285 1286 /** 1287 * State persisted across instances 1288 */ 1289 protected static class SavedState extends BaseSavedState { 1290 int openDrawerGravity = Gravity.NO_GRAVITY; 1291 int lockModeLeft = LOCK_MODE_UNLOCKED; 1292 int lockModeRight = LOCK_MODE_UNLOCKED; 1293 1294 public SavedState(Parcel in) { 1295 super(in); 1296 openDrawerGravity = in.readInt(); 1297 } 1298 1299 public SavedState(Parcelable superState) { 1300 super(superState); 1301 } 1302 1303 @Override 1304 public void writeToParcel(Parcel dest, int flags) { 1305 super.writeToParcel(dest, flags); 1306 dest.writeInt(openDrawerGravity); 1307 } 1308 1309 public static final Parcelable.Creator<SavedState> CREATOR = 1310 new Parcelable.Creator<SavedState>() { 1311 @Override 1312 public SavedState createFromParcel(Parcel source) { 1313 return new SavedState(source); 1314 } 1315 1316 @Override 1317 public SavedState[] newArray(int size) { 1318 return new SavedState[size]; 1319 } 1320 }; 1321 } 1322 1323 private class ViewDragCallback extends ViewDragHelper.Callback { 1324 private final int mAbsGravity; 1325 private ViewDragHelper mDragger; 1326 1327 private final Runnable mPeekRunnable = new Runnable() { 1328 @Override public void run() { 1329 peekDrawer(); 1330 } 1331 }; 1332 1333 public ViewDragCallback(int gravity) { 1334 mAbsGravity = gravity; 1335 } 1336 1337 public void setDragger(ViewDragHelper dragger) { 1338 mDragger = dragger; 1339 } 1340 1341 public void removeCallbacks() { 1342 DrawerLayout.this.removeCallbacks(mPeekRunnable); 1343 } 1344 1345 @Override 1346 public boolean tryCaptureView(View child, int pointerId) { 1347 // Only capture views where the gravity matches what we're looking for. 1348 // This lets us use two ViewDragHelpers, one for each side drawer. 1349 return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) 1350 && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; 1351 } 1352 1353 @Override 1354 public void onViewDragStateChanged(int state) { 1355 updateDrawerState(mAbsGravity, state, mDragger.getCapturedView()); 1356 } 1357 1358 @Override 1359 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 1360 float offset; 1361 final int childWidth = changedView.getWidth(); 1362 1363 // This reverses the positioning shown in onLayout. 1364 if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) { 1365 offset = (float) (childWidth + left) / childWidth; 1366 } else { 1367 final int width = getWidth(); 1368 offset = (float) (width - left) / childWidth; 1369 } 1370 setDrawerViewOffset(changedView, offset); 1371 changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); 1372 invalidate(); 1373 } 1374 1375 @Override 1376 public void onViewCaptured(View capturedChild, int activePointerId) { 1377 final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); 1378 lp.isPeeking = false; 1379 1380 closeOtherDrawer(); 1381 } 1382 1383 private void closeOtherDrawer() { 1384 final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; 1385 final View toClose = findDrawerWithGravity(otherGrav); 1386 if (toClose != null) { 1387 closeDrawer(toClose); 1388 } 1389 } 1390 1391 @Override 1392 public void onViewReleased(View releasedChild, float xvel, float yvel) { 1393 // Offset is how open the drawer is, therefore left/right values 1394 // are reversed from one another. 1395 final float offset = getDrawerViewOffset(releasedChild); 1396 final int childWidth = releasedChild.getWidth(); 1397 1398 int left; 1399 if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) { 1400 left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; 1401 } else { 1402 final int width = getWidth(); 1403 left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width; 1404 } 1405 1406 mDragger.settleCapturedViewAt(left, releasedChild.getTop()); 1407 invalidate(); 1408 } 1409 1410 @Override 1411 public void onEdgeTouched(int edgeFlags, int pointerId) { 1412 postDelayed(mPeekRunnable, PEEK_DELAY); 1413 } 1414 1415 private void peekDrawer() { 1416 final View toCapture; 1417 final int childLeft; 1418 final int peekDistance = mDragger.getEdgeSize(); 1419 final boolean leftEdge = mAbsGravity == Gravity.LEFT; 1420 if (leftEdge) { 1421 toCapture = findDrawerWithGravity(Gravity.LEFT); 1422 childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; 1423 } else { 1424 toCapture = findDrawerWithGravity(Gravity.RIGHT); 1425 childLeft = getWidth() - peekDistance; 1426 } 1427 // Only peek if it would mean making the drawer more visible and the drawer isn't locked 1428 if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || 1429 (!leftEdge && toCapture.getLeft() > childLeft)) && 1430 getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { 1431 final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); 1432 mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); 1433 lp.isPeeking = true; 1434 invalidate(); 1435 1436 closeOtherDrawer(); 1437 1438 cancelChildViewTouch(); 1439 } 1440 } 1441 1442 @Override 1443 public boolean onEdgeLock(int edgeFlags) { 1444 if (ALLOW_EDGE_LOCK) { 1445 final View drawer = findDrawerWithGravity(mAbsGravity); 1446 if (drawer != null && !isDrawerOpen(drawer)) { 1447 closeDrawer(drawer); 1448 } 1449 return true; 1450 } 1451 return false; 1452 } 1453 1454 @Override 1455 public void onEdgeDragStarted(int edgeFlags, int pointerId) { 1456 final View toCapture; 1457 if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { 1458 toCapture = findDrawerWithGravity(Gravity.LEFT); 1459 } else { 1460 toCapture = findDrawerWithGravity(Gravity.RIGHT); 1461 } 1462 1463 if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { 1464 mDragger.captureChildView(toCapture, pointerId); 1465 } 1466 } 1467 1468 @Override 1469 public int getViewHorizontalDragRange(View child) { 1470 return child.getWidth(); 1471 } 1472 1473 @Override 1474 public int clampViewPositionHorizontal(View child, int left, int dx) { 1475 if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { 1476 return Math.max(-child.getWidth(), Math.min(left, 0)); 1477 } else { 1478 final int width = getWidth(); 1479 return Math.max(width - child.getWidth(), Math.min(left, width)); 1480 } 1481 } 1482 1483 @Override 1484 public int clampViewPositionVertical(View child, int top, int dy) { 1485 return child.getTop(); 1486 } 1487 } 1488 1489 public static class LayoutParams extends ViewGroup.MarginLayoutParams { 1490 1491 public int gravity = Gravity.NO_GRAVITY; 1492 float onScreen; 1493 boolean isPeeking; 1494 boolean knownOpen; 1495 1496 public LayoutParams(Context c, AttributeSet attrs) { 1497 super(c, attrs); 1498 1499 final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 1500 this.gravity = a.getInt(0, Gravity.NO_GRAVITY); 1501 a.recycle(); 1502 } 1503 1504 public LayoutParams(int width, int height) { 1505 super(width, height); 1506 } 1507 1508 public LayoutParams(int width, int height, int gravity) { 1509 this(width, height); 1510 this.gravity = gravity; 1511 } 1512 1513 public LayoutParams(LayoutParams source) { 1514 super(source); 1515 this.gravity = source.gravity; 1516 } 1517 1518 public LayoutParams(ViewGroup.LayoutParams source) { 1519 super(source); 1520 } 1521 1522 public LayoutParams(ViewGroup.MarginLayoutParams source) { 1523 super(source); 1524 } 1525 } 1526 1527 class AccessibilityDelegate extends AccessibilityDelegateCompat { 1528 private final Rect mTmpRect = new Rect(); 1529 1530 @Override 1531 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 1532 final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); 1533 super.onInitializeAccessibilityNodeInfo(host, superNode); 1534 1535 info.setSource(host); 1536 final ViewParent parent = ViewCompat.getParentForAccessibility(host); 1537 if (parent instanceof View) { 1538 info.setParent((View) parent); 1539 } 1540 copyNodeInfoNoChildren(info, superNode); 1541 1542 superNode.recycle(); 1543 1544 addChildrenForAccessibility(info, (ViewGroup) host); 1545 } 1546 1547 private void addChildrenForAccessibility(AccessibilityNodeInfoCompat info, ViewGroup v) { 1548 final int childCount = v.getChildCount(); 1549 for (int i = 0; i < childCount; i++) { 1550 final View child = v.getChildAt(i); 1551 if (filter(child)) { 1552 continue; 1553 } 1554 1555 // Adding children that are marked as not important for 1556 // accessibility will break the hierarchy, so we need to check 1557 // that value and re-parent views if necessary. 1558 final int importance = ViewCompat.getImportantForAccessibility(child); 1559 switch (importance) { 1560 case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS: 1561 // Always skip NO_HIDE views and their descendants. 1562 break; 1563 case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO: 1564 // Re-parent children of NO view groups, skip NO views. 1565 if (child instanceof ViewGroup) { 1566 addChildrenForAccessibility(info, (ViewGroup) child); 1567 } 1568 break; 1569 case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO: 1570 // Force AUTO views to YES and add them. 1571 ViewCompat.setImportantForAccessibility( 1572 child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 1573 case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES: 1574 info.addChild(child); 1575 break; 1576 } 1577 } 1578 } 1579 1580 @Override 1581 public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, 1582 AccessibilityEvent event) { 1583 if (!filter(child)) { 1584 return super.onRequestSendAccessibilityEvent(host, child, event); 1585 } 1586 return false; 1587 } 1588 1589 public boolean filter(View child) { 1590 final View openDrawer = findOpenDrawer(); 1591 return openDrawer != null && openDrawer != child; 1592 } 1593 1594 /** 1595 * This should really be in AccessibilityNodeInfoCompat, but there unfortunately 1596 * seem to be a few elements that are not easily cloneable using the underlying API. 1597 * Leave it private here as it's not general-purpose useful. 1598 */ 1599 private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, 1600 AccessibilityNodeInfoCompat src) { 1601 final Rect rect = mTmpRect; 1602 1603 src.getBoundsInParent(rect); 1604 dest.setBoundsInParent(rect); 1605 1606 src.getBoundsInScreen(rect); 1607 dest.setBoundsInScreen(rect); 1608 1609 dest.setVisibleToUser(src.isVisibleToUser()); 1610 dest.setPackageName(src.getPackageName()); 1611 dest.setClassName(src.getClassName()); 1612 dest.setContentDescription(src.getContentDescription()); 1613 1614 dest.setEnabled(src.isEnabled()); 1615 dest.setClickable(src.isClickable()); 1616 dest.setFocusable(src.isFocusable()); 1617 dest.setFocused(src.isFocused()); 1618 dest.setAccessibilityFocused(src.isAccessibilityFocused()); 1619 dest.setSelected(src.isSelected()); 1620 dest.setLongClickable(src.isLongClickable()); 1621 1622 dest.addAction(src.getActions()); 1623 } 1624 } 1625} 1626