DrawerLayout.java revision 791f31bbba40b8b51694a1b2cdc804f360786ed1
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.drawable.Drawable; 26import android.support.v4.view.GravityCompat; 27import android.support.v4.view.KeyEventCompat; 28import android.support.v4.view.MotionEventCompat; 29import android.support.v4.view.ViewCompat; 30import android.util.AttributeSet; 31import android.view.Gravity; 32import android.view.KeyEvent; 33import android.view.MotionEvent; 34import android.view.View; 35import android.view.ViewGroup; 36 37/** 38 * DrawerLayout acts as a top-level container for window content that allows for 39 * interactive "drawer" views to be pulled out from the edge of the window. 40 * 41 * <p>Drawer positioning and layout is controlled using the <code>android:layout_gravity</code> 42 * attribute on child views corresponding to </p> 43 * 44 * <p>As per the Android Design guide, any drawers positioned to the left/start should 45 * always contain content for navigating around the application, whereas any drawers 46 * positioned to the right/end should always contain actions to take on the current content. 47 * This preserves the same navigation left, actions right structure present in the Action Bar 48 * and elsewhere.</p> 49 */ 50public class DrawerLayout extends ViewGroup { 51 private static final String TAG = "DrawerLayout"; 52 53 private static final int INVALID_POINTER = -1; 54 55 /** 56 * Indicates that any drawers are in an idle, settled state. No animation is in progress. 57 */ 58 public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; 59 60 /** 61 * Indicates that a drawer is currently being dragged by the user. 62 */ 63 public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; 64 65 /** 66 * Indicates that a drawer is in the process of settling to a final position. 67 */ 68 public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; 69 70 private static final int MIN_DRAWER_MARGIN = 64; // dp 71 72 private static final int DRAWER_PEEK_DISTANCE = 16; // dp 73 74 private static final int DEFAULT_SCRIM_COLOR = 0x99000000; 75 76 private static final int[] LAYOUT_ATTRS = new int[] { 77 android.R.attr.layout_gravity 78 }; 79 80 private int mMinDrawerMargin; 81 private int mDrawerPeekDistance; 82 83 private int mScrimColor = DEFAULT_SCRIM_COLOR; 84 private float mScrimOpacity; 85 private Paint mScrimPaint = new Paint(); 86 87 private final ViewDragHelper mLeftDragger; 88 private final ViewDragHelper mRightDragger; 89 private int mDrawerState; 90 private boolean mInLayout; 91 92 private DrawerListener mListener; 93 94 private float mInitialMotionX; 95 private float mInitialMotionY; 96 97 private Drawable mShadowLeft; 98 private Drawable mShadowRight; 99 100 /** 101 * Listener for monitoring events about drawers. 102 */ 103 public interface DrawerListener { 104 /** 105 * Called when a drawer's position changes. 106 * @param drawerView The child view that was moved 107 * @param slideOffset The new offset of this drawer within its range, from 0-1 108 */ 109 public void onDrawerSlide(View drawerView, float slideOffset); 110 111 /** 112 * Called when a drawer has settled in a completely open state. 113 * The drawer is interactive at this point. 114 * 115 * @param drawerView Drawer view that is now open 116 */ 117 public void onDrawerOpened(View drawerView); 118 119 /** 120 * Called when a drawer has settled in a completely closed state. 121 * 122 * @param drawerView Drawer view that is now closed 123 */ 124 public void onDrawerClosed(View drawerView); 125 126 /** 127 * Called when the drawer motion state changes. The new state will 128 * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. 129 * 130 * @param newState The new drawer motion state 131 */ 132 public void onDrawerStateChanged(int newState); 133 } 134 135 /** 136 * Stub/no-op implementations of all methods of {@link DrawerListener}. 137 * Override this if you only care about a few of the available callback methods. 138 */ 139 public static abstract class SimpleDrawerListener implements DrawerListener { 140 @Override 141 public void onDrawerSlide(View drawerView, float slideOffset) { 142 } 143 144 @Override 145 public void onDrawerOpened(View drawerView) { 146 } 147 148 @Override 149 public void onDrawerClosed(View drawerView) { 150 } 151 152 @Override 153 public void onDrawerStateChanged(int newState) { 154 } 155 } 156 157 public DrawerLayout(Context context) { 158 this(context, null); 159 } 160 161 public DrawerLayout(Context context, AttributeSet attrs) { 162 this(context, attrs, 0); 163 } 164 165 public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { 166 super(context, attrs, defStyle); 167 168 final float density = getResources().getDisplayMetrics().density; 169 mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); 170 mDrawerPeekDistance = (int) (DRAWER_PEEK_DISTANCE * density + 0.5f); 171 172 final ViewDragCallback leftCallback = new ViewDragCallback(Gravity.LEFT); 173 final ViewDragCallback rightCallback = new ViewDragCallback(Gravity.RIGHT); 174 175 mLeftDragger = ViewDragHelper.create(this, 0.5f, leftCallback); 176 mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); 177 leftCallback.setDragger(mLeftDragger); 178 179 mRightDragger = ViewDragHelper.create(this, 0.5f, rightCallback); 180 mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); 181 rightCallback.setDragger(mRightDragger); 182 183 // So that we can catch the back button 184 setFocusableInTouchMode(true); 185 } 186 187 /** 188 * Set a simple drawable used for the left or right shadow. 189 * The drawable provided must have a nonzero intrinsic width. 190 * 191 * @param shadowDrawable Shadow drawable to use at the edge of a drawer 192 * @param gravity Which drawer the shadow should apply to 193 */ 194 public void setDrawerShadow(Drawable shadowDrawable, int gravity) { 195 /* 196 * TODO Someone someday might want to set more complex drawables here. 197 * They're probably nuts, but we might want to consider registering callbacks, 198 * setting states, etc. properly. 199 */ 200 201 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 202 ViewCompat.getLayoutDirection(this)); 203 if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { 204 mShadowLeft = shadowDrawable; 205 invalidate(); 206 } 207 if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { 208 mShadowRight = shadowDrawable; 209 invalidate(); 210 } 211 } 212 213 /** 214 * Set a simple drawable used for the left or right shadow. 215 * The drawable provided must have a nonzero intrinsic width. 216 * 217 * @param resId Resource id of a shadow drawable to use at the edge of a drawer 218 * @param gravity Which drawer the shadow should apply to 219 */ 220 public void setDrawerShadow(int resId, int gravity) { 221 setDrawerShadow(getResources().getDrawable(resId), gravity); 222 } 223 224 /** 225 * Set a listener to be notified of drawer events. 226 * 227 * @param listener Listener to notify when drawer events occur 228 * @see DrawerListener 229 */ 230 public void setDrawerListener(DrawerListener listener) { 231 mListener = listener; 232 } 233 234 /** 235 * Resolve the shared state of all drawers from the component ViewDragHelpers. 236 * Should be called whenever a ViewDragHelper's state changes. 237 */ 238 void updateDrawerState(int forGravity, int activeState, View activeDrawer) { 239 final int leftState = mLeftDragger.getViewDragState(); 240 final int rightState = mRightDragger.getViewDragState(); 241 242 final int state; 243 if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) { 244 state = STATE_DRAGGING; 245 } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) { 246 state = STATE_SETTLING; 247 } else { 248 state = STATE_IDLE; 249 } 250 251 if (activeDrawer != null && activeState == STATE_IDLE) { 252 final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams(); 253 if (lp.onScreen == 0) { 254 dispatchOnDrawerClosed(activeDrawer); 255 } else if (lp.onScreen == 1) { 256 dispatchOnDrawerOpened(activeDrawer); 257 } 258 } 259 260 if (state != mDrawerState) { 261 mDrawerState = state; 262 263 if (mListener != null) { 264 mListener.onDrawerStateChanged(state); 265 } 266 } 267 } 268 269 void dispatchOnDrawerClosed(View drawerView) { 270 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 271 if (lp.knownOpen) { 272 lp.knownOpen = false; 273 if (mListener != null) { 274 mListener.onDrawerClosed(drawerView); 275 } 276 } 277 } 278 279 void dispatchOnDrawerOpened(View drawerView) { 280 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 281 if (!lp.knownOpen) { 282 lp.knownOpen = true; 283 if (mListener != null) { 284 mListener.onDrawerOpened(drawerView); 285 } 286 } 287 } 288 289 void dispatchOnDrawerSlide(View drawerView, float slideOffset) { 290 if (mListener != null) { 291 mListener.onDrawerSlide(drawerView, slideOffset); 292 } 293 } 294 295 void setDrawerViewOffset(View drawerView, float slideOffset) { 296 final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); 297 if (slideOffset == lp.onScreen) { 298 return; 299 } 300 301 lp.onScreen = slideOffset; 302 dispatchOnDrawerSlide(drawerView, slideOffset); 303 } 304 305 float getDrawerViewOffset(View drawerView) { 306 return ((LayoutParams) drawerView.getLayoutParams()).onScreen; 307 } 308 309 int getDrawerViewGravity(View drawerView) { 310 final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; 311 return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(drawerView)); 312 } 313 314 boolean checkDrawerViewGravity(View drawerView, int checkFor) { 315 final int absGrav = getDrawerViewGravity(drawerView); 316 return (absGrav & checkFor) == checkFor; 317 } 318 319 void moveDrawerToOffset(View drawerView, float slideOffset) { 320 final float oldOffset = getDrawerViewOffset(drawerView); 321 final int width = drawerView.getWidth(); 322 final int oldPos = (int) (width * oldOffset); 323 final int newPos = (int) (width * slideOffset); 324 final int dx = newPos - oldPos; 325 326 drawerView.offsetLeftAndRight(checkDrawerViewGravity(drawerView, Gravity.LEFT) ? dx : -dx); 327 setDrawerViewOffset(drawerView, slideOffset); 328 } 329 330 View findDrawerWithGravity(int gravity) { 331 final int childCount = getChildCount(); 332 for (int i = 0; i < childCount; i++) { 333 final View child = getChildAt(i); 334 final int childGravity = getDrawerViewGravity(child); 335 if ((childGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 336 (gravity & Gravity.HORIZONTAL_GRAVITY_MASK)) { 337 return child; 338 } 339 } 340 return null; 341 } 342 343 /** 344 * Simple gravity to string - only supports LEFT and RIGHT for debugging output. 345 * 346 * @param gravity Absolute gravity value 347 * @return LEFT or RIGHT as appropriate, or a hex string 348 */ 349 static String gravityToString(int gravity) { 350 if ((gravity & Gravity.LEFT) == Gravity.LEFT) { 351 return "LEFT"; 352 } 353 if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { 354 return "RIGHT"; 355 } 356 return Integer.toHexString(gravity); 357 } 358 359 @Override 360 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 361 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 362 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 363 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 364 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 365 366 if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { 367 throw new IllegalArgumentException( 368 "DrawerLayout must be measured with MeasureSpec.EXACTLY."); 369 } 370 371 setMeasuredDimension(widthSize, heightSize); 372 373 // Gravity value for each drawer we've seen. Only one of each permitted. 374 int foundDrawers = 0; 375 final int childCount = getChildCount(); 376 for (int i = 0; i < childCount; i++) { 377 final View child = getChildAt(i); 378 379 if (child.getVisibility() == GONE) { 380 continue; 381 } 382 383 if (isContentView(child)) { 384 // Content views get measured at exactly the layout's size. 385 child.measure(widthMeasureSpec, heightMeasureSpec); 386 } else if (isDrawerView(child)) { 387 final int childGravity = 388 getDrawerViewGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; 389 if ((foundDrawers & childGravity) != 0) { 390 throw new IllegalStateException("Child drawer has absolute gravity " + 391 gravityToString(childGravity) + " but this " + TAG + " already has a " + 392 "drawer view along that edge"); 393 } 394 final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, mMinDrawerMargin, 395 child.getLayoutParams().width); 396 child.measure(drawerWidthSpec, heightMeasureSpec); 397 } else { 398 throw new IllegalStateException("Child " + child + " at index " + i + 399 " does not have a valid layout_gravity - must be Gravity.LEFT, " + 400 "Gravity.RIGHT or Gravity.NO_GRAVITY"); 401 } 402 } 403 } 404 405 @Override 406 protected void onLayout(boolean changed, int l, int t, int r, int b) { 407 mInLayout = true; 408 final int childCount = getChildCount(); 409 for (int i = 0; i < childCount; i++) { 410 final View child = getChildAt(i); 411 412 if (child.getVisibility() == GONE) { 413 continue; 414 } 415 416 if (isContentView(child)) { 417 child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); 418 } else { // Drawer, if it wasn't onMeasure would have thrown an exception. 419 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 420 421 final int childWidth = child.getMeasuredWidth(); 422 int childLeft; 423 424 if (checkDrawerViewGravity(child, Gravity.LEFT)) { 425 childLeft = -childWidth + (int) (childWidth * lp.onScreen); 426 } else { // Right; onMeasure checked for us. 427 childLeft = r - l - (int) (childWidth * lp.onScreen); 428 } 429 430 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); 431 432 if (lp.onScreen == 0) { 433 child.setVisibility(INVISIBLE); 434 } 435 } 436 } 437 mInLayout = false; 438 } 439 440 @Override 441 public void requestLayout() { 442 if (!mInLayout) { 443 super.requestLayout(); 444 } 445 } 446 447 @Override 448 public void computeScroll() { 449 final int childCount = getChildCount(); 450 float scrimOpacity = 0; 451 for (int i = 0; i < childCount; i++) { 452 final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; 453 scrimOpacity = Math.max(scrimOpacity, onscreen); 454 } 455 mScrimOpacity = scrimOpacity; 456 457 // "|" used on purpose; both need to run. 458 if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { 459 ViewCompat.postInvalidateOnAnimation(this); 460 } 461 } 462 463 private static boolean hasOpaqueBackground(View v) { 464 final Drawable bg = v.getBackground(); 465 if (bg != null) { 466 return bg.getOpacity() == PixelFormat.OPAQUE; 467 } 468 return false; 469 } 470 471 @Override 472 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 473 final boolean drawingContent = isContentView(child); 474 int clipLeft = 0, clipRight = getWidth(); 475 476 final int restoreCount = canvas.save(); 477 if (drawingContent) { 478 final int childCount = getChildCount(); 479 for (int i = 0; i < childCount; i++) { 480 final View v = getChildAt(i); 481 if (v == child || v.getVisibility() != VISIBLE || 482 !hasOpaqueBackground(v) || !isDrawerView(v)) { 483 continue; 484 } 485 486 if (checkDrawerViewGravity(v, Gravity.LEFT)) { 487 final int vright = v.getRight(); 488 if (vright > clipLeft) clipLeft = vright; 489 } else { 490 final int vleft = v.getLeft(); 491 if (vleft < clipRight) clipRight = vleft; 492 } 493 } 494 canvas.clipRect(clipLeft, 0, clipRight, getHeight()); 495 } 496 final boolean result = super.drawChild(canvas, child, drawingTime); 497 canvas.restoreToCount(restoreCount); 498 499 if (mScrimOpacity > 0 && drawingContent) { 500 final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; 501 final int imag = (int) (baseAlpha * mScrimOpacity); 502 final int color = imag << 24 | (mScrimColor & 0xffffff); 503 mScrimPaint.setColor(color); 504 505 canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); 506 } else if (mShadowLeft != null && checkDrawerViewGravity(child, Gravity.LEFT)) { 507 final int shadowWidth = mShadowLeft.getIntrinsicWidth(); 508 final int childRight = child.getRight(); 509 final float alpha = 510 Math.max(0, Math.min((float) childRight / mDrawerPeekDistance, 1.f)); 511 mShadowLeft.setBounds(childRight, child.getTop(), 512 childRight + shadowWidth, child.getBottom()); 513 mShadowLeft.setAlpha((int) (0xff * alpha)); 514 mShadowLeft.draw(canvas); 515 } else if (mShadowRight != null && checkDrawerViewGravity(child, Gravity.RIGHT)) { 516 final int shadowWidth = mShadowRight.getIntrinsicWidth(); 517 final int childLeft = child.getLeft(); 518 final int showing = getWidth() - childLeft; 519 final float alpha = 520 Math.max(0, Math.min((float) showing / mDrawerPeekDistance, 1.f)); 521 mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), 522 childLeft, child.getBottom()); 523 mShadowRight.setAlpha((int) (0xff * alpha)); 524 mShadowRight.draw(canvas); 525 } 526 return result; 527 } 528 529 boolean isContentView(View child) { 530 return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; 531 } 532 533 boolean isDrawerView(View child) { 534 final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; 535 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 536 ViewCompat.getLayoutDirection(child)); 537 return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; 538 } 539 540 @Override 541 public boolean onInterceptTouchEvent(MotionEvent ev) { 542 final int action = MotionEventCompat.getActionMasked(ev); 543 544 // "|" used deliberately here; both methods should be invoked. 545 final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | 546 mRightDragger.shouldInterceptTouchEvent(ev); 547 548 boolean interceptForTap = false; 549 550 switch (action) { 551 case MotionEvent.ACTION_DOWN: { 552 final float x = ev.getX(); 553 final float y = ev.getY(); 554 mInitialMotionX = x; 555 mInitialMotionY = y; 556 if (mScrimOpacity > 0 && 557 isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) { 558 interceptForTap = true; 559 } 560 break; 561 } 562 563 case MotionEvent.ACTION_CANCEL: 564 case MotionEvent.ACTION_UP: { 565 closeDrawers(true); 566 } 567 } 568 return interceptForDrag || interceptForTap; 569 } 570 571 @Override 572 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 573 final int childCount = getChildCount(); 574 for (int i = 0; i < childCount; i++) { 575 final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); 576 577 if (lp.isPeeking) { 578 // Don't disallow intercept at all if we have a peeking view, we're probably 579 // going to intercept it later anyway. 580 return; 581 } 582 } 583 super.requestDisallowInterceptTouchEvent(disallowIntercept); 584 if (disallowIntercept) { 585 closeDrawers(true); 586 } 587 } 588 589 @Override 590 public boolean onTouchEvent(MotionEvent ev) { 591 mLeftDragger.processTouchEvent(ev); 592 mRightDragger.processTouchEvent(ev); 593 594 final int action = ev.getAction(); 595 boolean wantTouchEvents = true; 596 597 switch (action & MotionEventCompat.ACTION_MASK) { 598 case MotionEvent.ACTION_DOWN: { 599 final float x = ev.getX(); 600 final float y = ev.getY(); 601 mInitialMotionX = x; 602 mInitialMotionY = y; 603 break; 604 } 605 606 case MotionEvent.ACTION_UP: { 607 final float x = ev.getX(); 608 final float y = ev.getY(); 609 boolean peekingOnly = true; 610 if (isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) { 611 final float dx = x - mInitialMotionX; 612 final float dy = y - mInitialMotionY; 613 final int slop = mLeftDragger.getTouchSlop(); 614 if (dx * dx + dy * dy < slop * slop) { 615 // Taps close a dimmed open pane. 616 peekingOnly = false; 617 } 618 } 619 closeDrawers(peekingOnly); 620 break; 621 } 622 623 case MotionEvent.ACTION_CANCEL: { 624 closeDrawers(true); 625 break; 626 } 627 } 628 629 return wantTouchEvents; 630 } 631 632 /** 633 * Close all currently open drawer views by animating them out of view. 634 */ 635 public void closeDrawers() { 636 closeDrawers(false); 637 } 638 639 void closeDrawers(boolean peekingOnly) { 640 boolean needsInvalidate = false; 641 final int childCount = getChildCount(); 642 for (int i = 0; i < childCount; i++) { 643 final View child = getChildAt(i); 644 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 645 646 if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { 647 continue; 648 } 649 650 final int childWidth = child.getWidth(); 651 652 if (checkDrawerViewGravity(child, Gravity.LEFT)) { 653 needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, 654 -childWidth, child.getTop()); 655 } else { 656 needsInvalidate |= mRightDragger.smoothSlideViewTo(child, 657 getWidth(), child.getTop()); 658 } 659 660 lp.isPeeking = false; 661 } 662 663 if (needsInvalidate) { 664 invalidate(); 665 } 666 } 667 668 /** 669 * Open the specified drawer view by animating it into view. 670 * 671 * @param drawerView Drawer view to open 672 */ 673 public void openDrawer(View drawerView) { 674 if (!isDrawerView(drawerView)) { 675 throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); 676 } 677 678 if (checkDrawerViewGravity(drawerView, Gravity.LEFT)) { 679 mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); 680 } else { 681 mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), 682 drawerView.getTop()); 683 } 684 invalidate(); 685 } 686 687 /** 688 * Open the specified drawer by animating it out of view. 689 * 690 * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. 691 * GravityCompat.START or GravityCompat.END may also be used. 692 */ 693 public void openDrawer(int gravity) { 694 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 695 ViewCompat.getLayoutDirection(this)); 696 final View drawerView = findDrawerWithGravity(absGravity); 697 698 if (drawerView == null) { 699 throw new IllegalArgumentException("No drawer view found with absolute gravity " + 700 gravityToString(absGravity)); 701 } 702 openDrawer(drawerView); 703 } 704 705 /** 706 * Close the specified drawer view by animating it into view. 707 * 708 * @param drawerView Drawer view to close 709 */ 710 public void closeDrawer(View drawerView) { 711 if (!isDrawerView(drawerView)) { 712 throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); 713 } 714 715 if (checkDrawerViewGravity(drawerView, Gravity.LEFT)) { 716 mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), drawerView.getTop()); 717 } else { 718 mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); 719 } 720 invalidate(); 721 } 722 723 /** 724 * Close the specified drawer by animating it out of view. 725 * 726 * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. 727 * GravityCompat.START or GravityCompat.END may also be used. 728 */ 729 public void closeDrawer(int gravity) { 730 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 731 ViewCompat.getLayoutDirection(this)); 732 final View drawerView = findDrawerWithGravity(absGravity); 733 734 if (drawerView == null) { 735 throw new IllegalArgumentException("No drawer view found with absolute gravity " + 736 gravityToString(absGravity)); 737 } 738 closeDrawer(drawerView); 739 } 740 741 /** 742 * Check if the given drawer view is currently in an open state. 743 * To be considered "open" the drawer must have settled into its fully 744 * visible state. To check for partial visibility use 745 * {@link #isDrawerVisible(android.view.View)}. 746 * 747 * @param drawer Drawer view to check 748 * @return true if the given drawer view is in an open state 749 * @see #isDrawerVisible(android.view.View) 750 */ 751 public boolean isDrawerOpen(View drawer) { 752 if (!isDrawerView(drawer)) { 753 throw new IllegalArgumentException("View " + drawer + " is not a drawer"); 754 } 755 return ((LayoutParams) drawer.getLayoutParams()).knownOpen; 756 } 757 758 /** 759 * Check if a given drawer view is currently visible on-screen. The drawer 760 * may be only peeking onto the screen, fully extended, or anywhere inbetween. 761 * 762 * @param drawer Drawer view to check 763 * @return true if the given drawer is visible on-screen 764 * @see #isDrawerOpen(android.view.View) 765 */ 766 public boolean isDrawerVisible(View drawer) { 767 if (!isDrawerView(drawer)) { 768 throw new IllegalArgumentException("View " + drawer + " is not a drawer"); 769 } 770 return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; 771 } 772 773 @Override 774 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 775 return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); 776 } 777 778 @Override 779 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 780 return p instanceof LayoutParams 781 ? new LayoutParams((LayoutParams) p) 782 : new LayoutParams(p); 783 } 784 785 @Override 786 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 787 return p instanceof LayoutParams && super.checkLayoutParams(p); 788 } 789 790 @Override 791 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 792 return new LayoutParams(getContext(), attrs); 793 } 794 795 private boolean hasVisibleDrawer() { 796 final int childCount = getChildCount(); 797 for (int i = 0; i < childCount; i++) { 798 final View child = getChildAt(i); 799 if (isDrawerView(child) && isDrawerVisible(child)) { 800 return true; 801 } 802 } 803 return false; 804 } 805 806 @Override 807 public boolean onKeyDown(int keyCode, KeyEvent event) { 808 if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { 809 KeyEventCompat.startTracking(event); 810 return true; 811 } 812 return super.onKeyDown(keyCode, event); 813 } 814 815 @Override 816 public boolean onKeyUp(int keyCode, KeyEvent event) { 817 if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { 818 closeDrawers(); 819 return true; 820 } 821 return super.onKeyUp(keyCode, event); 822 } 823 824 private class ViewDragCallback extends ViewDragHelper.Callback { 825 826 private final int mGravity; 827 private ViewDragHelper mDragger; 828 829 public ViewDragCallback(int gravity) { 830 mGravity = gravity; 831 } 832 833 public void setDragger(ViewDragHelper dragger) { 834 mDragger = dragger; 835 } 836 837 @Override 838 public boolean tryCaptureView(View child, int pointerId) { 839 // Only capture views where the gravity matches what we're looking for. 840 // This lets us use two ViewDragHelpers, one for each side drawer. 841 return isDrawerView(child) && checkDrawerViewGravity(child, mGravity); 842 } 843 844 @Override 845 public void onViewDragStateChanged(int state) { 846 updateDrawerState(mGravity, state, mDragger.getCapturedView()); 847 } 848 849 @Override 850 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 851 float offset; 852 final int childWidth = changedView.getWidth(); 853 854 // This reverses the positioning shown in onLayout. 855 if (checkDrawerViewGravity(changedView, Gravity.LEFT)) { 856 offset = (float) (childWidth + left) / childWidth; 857 } else { 858 final int width = getWidth(); 859 offset = (float) (width - left) / childWidth; 860 } 861 setDrawerViewOffset(changedView, offset); 862 changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); 863 invalidate(); 864 } 865 866 @Override 867 public void onViewCaptured(View capturedChild, int activePointerId) { 868 final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); 869 lp.isPeeking = false; 870 871 closeOtherDrawer(); 872 } 873 874 private void closeOtherDrawer() { 875 final int otherGrav = mGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; 876 final View toClose = findDrawerWithGravity(otherGrav); 877 if (toClose != null) { 878 closeDrawer(toClose); 879 } 880 } 881 882 @Override 883 public void onViewReleased(View releasedChild, float xvel, float yvel) { 884 // Offset is how open the drawer is, therefore left/right values 885 // are reversed from one another. 886 final float offset = getDrawerViewOffset(releasedChild); 887 final int childWidth = releasedChild.getWidth(); 888 889 int left; 890 if (checkDrawerViewGravity(releasedChild, Gravity.LEFT)) { 891 left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; 892 } else { 893 final int width = getWidth(); 894 left = xvel < 0 || xvel == 0 && offset < 0.5f ? width - childWidth : width; 895 } 896 897 mDragger.settleCapturedViewAt(left, releasedChild.getTop()); 898 invalidate(); 899 } 900 901 @Override 902 public void onEdgeTouched(int edgeFlags, int pointerId) { 903 final View toCapture; 904 final int childLeft; 905 final boolean leftEdge = 906 (edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT; 907 if (leftEdge) { 908 toCapture = findDrawerWithGravity(Gravity.LEFT); 909 childLeft = -toCapture.getWidth() + mDrawerPeekDistance; 910 } else { 911 toCapture = findDrawerWithGravity(Gravity.RIGHT); 912 childLeft = getWidth() - mDrawerPeekDistance; 913 } 914 915 // Only peek if it would mean making the drawer more visible 916 if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || 917 (!leftEdge && toCapture.getLeft() > childLeft))) { 918 final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); 919 mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); 920 lp.isPeeking = true; 921 invalidate(); 922 923 closeOtherDrawer(); 924 } 925 } 926 927 @Override 928 public void onEdgeDragStarted(int edgeFlags, int pointerId) { 929 final View toCapture; 930 if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { 931 toCapture = findDrawerWithGravity(Gravity.LEFT); 932 } else { 933 toCapture = findDrawerWithGravity(Gravity.RIGHT); 934 } 935 936 if (toCapture != null) { 937 mDragger.captureChildView(toCapture, pointerId); 938 } 939 } 940 941 @Override 942 public int getViewHorizontalDragRange(View child) { 943 return child.getWidth(); 944 } 945 946 @Override 947 public int clampViewPositionHorizontal(View child, int left, int dx) { 948 if (checkDrawerViewGravity(child, Gravity.LEFT)) { 949 return Math.max(-child.getWidth(), Math.min(left, 0)); 950 } else { 951 final int width = getWidth(); 952 return Math.max(width - child.getWidth(), Math.min(left, width)); 953 } 954 } 955 } 956 957 public static class LayoutParams extends ViewGroup.LayoutParams { 958 959 public int gravity = Gravity.NO_GRAVITY; 960 float onScreen; 961 boolean isPeeking; 962 boolean knownOpen; 963 964 public LayoutParams(Context c, AttributeSet attrs) { 965 super(c, attrs); 966 967 final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 968 this.gravity = a.getInt(0, Gravity.NO_GRAVITY); 969 a.recycle(); 970 } 971 972 public LayoutParams(int width, int height) { 973 super(width, height); 974 } 975 976 public LayoutParams(int width, int height, int gravity) { 977 this(width, height); 978 this.gravity = gravity; 979 } 980 981 public LayoutParams(LayoutParams source) { 982 super(source); 983 this.gravity = source.gravity; 984 } 985 986 public LayoutParams(ViewGroup.LayoutParams source) { 987 super(source); 988 } 989 } 990} 991