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