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