SlidingPaneLayout.java revision 3783c4b814bf70711fc4d674e9c7fbc34b1e5a83
1/* 2 * Copyright (C) 2012 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 17package android.support.v4.widget; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.graphics.Bitmap; 22import android.graphics.Canvas; 23import android.graphics.Paint; 24import android.graphics.PorterDuff; 25import android.graphics.PorterDuffColorFilter; 26import android.graphics.drawable.Drawable; 27import android.os.Build; 28import android.os.Parcel; 29import android.os.Parcelable; 30import android.support.v4.view.MotionEventCompat; 31import android.support.v4.view.VelocityTrackerCompat; 32import android.support.v4.view.ViewCompat; 33import android.support.v4.view.ViewConfigurationCompat; 34import android.util.AttributeSet; 35import android.util.Log; 36import android.view.MotionEvent; 37import android.view.VelocityTracker; 38import android.view.View; 39import android.view.ViewConfiguration; 40import android.view.ViewGroup; 41import android.view.animation.Interpolator; 42import android.widget.Scroller; 43 44import java.lang.reflect.Field; 45import java.lang.reflect.Method; 46 47/** 48 * SlidingPaneLayout provides a horizontal, multi-pane layout for use at the top level 49 * of a UI. A left (or first) pane is treated as a content list or browser, subordinate to a 50 * primary detail view for displaying content. 51 * 52 * <p>Child views may overlap if their combined width exceeds the available width 53 * in the SlidingPaneLayout. When this occurs the user may slide the topmost view out of the way 54 * by dragging it, or by navigating in the direction of the overlapped view using a keyboard. 55 * If the content of the dragged child view is itself horizontally scrollable, the user may 56 * grab it by the very edge.</p> 57 * 58 * <p>Thanks to this sliding behavior, SlidingPaneLayout may be suitable for creating layouts 59 * that can smoothly adapt across many different screen sizes, expanding out fully on larger 60 * screens and collapsing on smaller screens.</p> 61 * 62 * <p>SlidingPaneLayout is distinct from a navigation drawer as described in the design 63 * guide and should not be used in the same scenarios. SlidingPaneLayout should be thought 64 * of only as a way to allow a two-pane layout normally used on larger screens to adapt to smaller 65 * screens in a natural way. The interaction patterns expressed by SlidingPaneLayout imply 66 * a physicality and direct information hierarchy between panes that does not necessarily exist 67 * in a scenario where a navigation drawer should be used instead.</p> 68 * 69 * <p>Appropriate uses of SlidingPaneLayout include pairings of panes such as a contact list and 70 * subordinate interactions with those contacts, or an email thread list with the content pane 71 * displaying the contents of the selected thread. Inappropriate uses of SlidingPaneLayout include 72 * switching between disparate functions of your app, such as jumping from a social stream view 73 * to a view of your personal profile - cases such as this should use the navigation drawer 74 * pattern instead. (TODO: insert doc link to nav drawer widget.)</p> 75 * 76 * <p>Like {@link android.widget.LinearLayout LinearLayout}, SlidingPaneLayout supports 77 * the use of the layout parameter <code>layout_weight</code> on child views to determine 78 * how to divide leftover space after measurement is complete. It is only relevant for width. 79 * When views do not overlap weight behaves as it does in a LinearLayout.</p> 80 * 81 * <p>When views do overlap, weight on a slideable pane indicates that the pane should be 82 * sized to fill all available space in the closed state. Weight on a pane that becomes covered 83 * indicates that the pane should be sized to fill all available space except a small minimum strip 84 * that the user may use to grab the slideable view and pull it back over into a closed state.</p> 85 * 86 * <p>Experimental. This class may be removed.</p> 87 */ 88public class SlidingPaneLayout extends ViewGroup { 89 private static final String TAG = "SlidingPaneLayout"; 90 91 /** 92 * Default size of the touch gutter along the edge where the user 93 * may grab and drag a sliding pane, even if its internal content 94 * may horizontally scroll. 95 */ 96 private static final int DEFAULT_GUTTER_SIZE = 16; // dp 97 98 /** 99 * Default size of the overhang for a pane in the open state. 100 * At least this much of a sliding pane will remain visible. 101 * This indicates that there is more content available and provides 102 * a "physical" edge to grab to pull it closed. 103 */ 104 private static final int DEFAULT_OVERHANG_SIZE = 32; // dp; 105 106 private static final int MAX_SETTLE_DURATION = 600; // ms 107 108 /** 109 * If no fade color is given by default it will fade to 80% gray. 110 */ 111 private static final int DEFAULT_FADE_COLOR = 0xcccccccc; 112 113 /** 114 * Drawable used to draw the shadow between panes. 115 */ 116 private Drawable mShadowDrawable; 117 118 /** 119 * The size of the touch gutter in pixels 120 */ 121 private final int mGutterSize; 122 123 /** 124 * The size of the overhang in pixels. 125 * This is the minimum section of the sliding panel that will 126 * be visible in the open state to allow for a closing drag. 127 */ 128 private final int mOverhangSize; 129 130 /** 131 * True if a panel can slide with the current measurements 132 */ 133 private boolean mCanSlide; 134 135 /** 136 * The child view that can slide, if any. 137 */ 138 private View mSlideableView; 139 140 /** 141 * How far the panel is offset from its closed position. 142 * range [0, 1] where 0 = closed, 1 = open. 143 */ 144 private float mSlideOffset; 145 146 /** 147 * How far the non-sliding panel is parallaxed from its usual position when open. 148 * range [0, 1] 149 */ 150 private float mParallaxOffset; 151 152 /** 153 * How far in pixels the slideable panel may move. 154 */ 155 private int mSlideRange; 156 157 /** 158 * A panel view is locked into internal scrolling or another condition that 159 * is preventing a drag. 160 */ 161 private boolean mIsUnableToDrag; 162 163 /** 164 * Distance in pixels to parallax the fixed pane by when fully closed 165 */ 166 private int mParallaxBy; 167 168 private int mTouchSlop; 169 private float mInitialMotionX; 170 private float mInitialMotionY; 171 private float mLastMotionX; 172 private float mLastMotionY; 173 private int mActivePointerId = INVALID_POINTER; 174 175 private VelocityTracker mVelocityTracker; 176 private float mMaxVelocity; 177 178 private PanelSlideListener mPanelSlideListener; 179 180 private static final int INVALID_POINTER = -1; 181 182 /** 183 * Indicates that the panels are in an idle, settled state. The current panel 184 * is fully in view and no animation is in progress. 185 */ 186 public static final int SCROLL_STATE_IDLE = 0; 187 188 /** 189 * Indicates that a panel is currently being dragged by the user. 190 */ 191 public static final int SCROLL_STATE_DRAGGING = 1; 192 193 /** 194 * Indicates that a panel is in the process of settling to a final position. 195 */ 196 public static final int SCROLL_STATE_SETTLING = 2; 197 198 private int mScrollState = SCROLL_STATE_IDLE; 199 200 /** 201 * Interpolator defining the animation curve for mScroller 202 */ 203 private static final Interpolator sInterpolator = new Interpolator() { 204 public float getInterpolation(float t) { 205 t -= 1.0f; 206 return t * t * t * t * t + 1.0f; 207 } 208 }; 209 210 /** 211 * Used to animate flinging panes. 212 */ 213 private final Scroller mScroller; 214 215 static final SlidingPanelLayoutImpl IMPL; 216 217 static { 218 final int deviceVersion = Build.VERSION.SDK_INT; 219 if (deviceVersion >= 17) { 220 IMPL = new SlidingPanelLayoutImplJBMR1(); 221 } else if (deviceVersion >= 16) { 222 IMPL = new SlidingPanelLayoutImplJB(); 223 } else { 224 IMPL = new SlidingPanelLayoutImplBase(); 225 } 226 } 227 228 /** 229 * Listener for monitoring events about sliding panes. 230 */ 231 public interface PanelSlideListener { 232 /** 233 * Called when a sliding pane's position changes. 234 * @param panel The child view that was moved 235 * @param slideOffset The new offset of this sliding pane within its range, from 0-1 236 */ 237 public void onPanelSlide(View panel, float slideOffset); 238 /** 239 * Called when a sliding pane becomes slid completely open. The pane may or may not 240 * be interactive at this point depending on how much of the pane is visible. 241 * @param panel The child view that was slid to an open position, revealing other panes 242 */ 243 public void onPanelOpened(View panel); 244 245 /** 246 * Called when a sliding pane becomes slid completely closed. The pane is now guaranteed 247 * to be interactive. It may now obscure other views in the layout. 248 * @param panel The child view that was slid to a closed position 249 */ 250 public void onPanelClosed(View panel); 251 } 252 253 /** 254 * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset 255 * of the listener methods you can extend this instead of implement the full interface. 256 */ 257 public static class SimplePanelSlideListener implements PanelSlideListener { 258 @Override 259 public void onPanelSlide(View panel, float slideOffset) { 260 } 261 @Override 262 public void onPanelOpened(View panel) { 263 } 264 @Override 265 public void onPanelClosed(View panel) { 266 } 267 } 268 269 public SlidingPaneLayout(Context context) { 270 this(context, null); 271 } 272 273 public SlidingPaneLayout(Context context, AttributeSet attrs) { 274 this(context, attrs, 0); 275 } 276 277 public SlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) { 278 super(context, attrs, defStyle); 279 280 mScroller = new Scroller(context, sInterpolator); 281 282 final float density = context.getResources().getDisplayMetrics().density; 283 mGutterSize = (int) (DEFAULT_GUTTER_SIZE * density + 0.5f); 284 mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f); 285 286 final ViewConfiguration viewConfig = ViewConfiguration.get(context); 287 mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(viewConfig); 288 mMaxVelocity = viewConfig.getScaledMaximumFlingVelocity(); 289 290 setWillNotDraw(false); 291 } 292 293 /** 294 * Set a distance to parallax the lower pane by when the upper pane is in its 295 * fully closed state. The lower pane will scroll between this position and 296 * its fully open state. 297 * 298 * @param parallaxBy Distance to parallax by in pixels 299 */ 300 public void setParallaxDistance(int parallaxBy) { 301 mParallaxBy = parallaxBy; 302 requestLayout(); 303 } 304 305 /** 306 * @return The distance the lower pane will parallax by when the upper pane is fully closed. 307 * 308 * @see #setParallaxDistance(int) 309 */ 310 public int getParallaxDistance() { 311 return mParallaxBy; 312 } 313 314 void setScrollState(int state) { 315 if (mScrollState != state) { 316 mScrollState = state; 317 } 318 } 319 320 public void setPanelSlideListener(PanelSlideListener listener) { 321 mPanelSlideListener = listener; 322 } 323 324 void dispatchOnPanelSlide(View panel) { 325 if (mPanelSlideListener != null) { 326 mPanelSlideListener.onPanelSlide(panel, mSlideOffset); 327 } 328 } 329 330 void dispatchOnPanelOpened(View panel) { 331 if (mPanelSlideListener != null) { 332 mPanelSlideListener.onPanelOpened(panel); 333 } 334 } 335 336 void dispatchOnPanelClosed(View panel) { 337 if (mPanelSlideListener != null) { 338 mPanelSlideListener.onPanelClosed(panel); 339 } 340 } 341 342 @Override 343 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 344 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 345 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 346 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 347 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 348 349 if (widthMode != MeasureSpec.EXACTLY) { 350 throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); 351 } else if (heightMode == MeasureSpec.UNSPECIFIED) { 352 throw new IllegalStateException("Height must not be UNSPECIFIED"); 353 } 354 355 int layoutHeight = 0; 356 int maxLayoutHeight = -1; 357 switch (heightMode) { 358 case MeasureSpec.EXACTLY: 359 layoutHeight = maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); 360 break; 361 case MeasureSpec.AT_MOST: 362 maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); 363 break; 364 } 365 366 float weightSum = 0; 367 boolean canSlide = false; 368 int widthRemaining = widthSize - getPaddingLeft() - getPaddingRight(); 369 final int childCount = getChildCount(); 370 371 if (childCount > 2) { 372 Log.e(TAG, "onMeasure: More than two child views are not supported."); 373 } 374 375 // First pass. Measure based on child LayoutParams width/height. 376 // Weight will incur a second pass. 377 for (int i = 0; i < childCount; i++) { 378 final View child = getChildAt(i); 379 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 380 381 if (child.getVisibility() == GONE) { 382 lp.dimWhenOffset = false; 383 continue; 384 } 385 386 if (lp.weight > 0) { 387 weightSum += lp.weight; 388 389 // If we have no width, weight is the only contributor to the final size. 390 // Measure this view on the weight pass only. 391 if (lp.width == 0) continue; 392 } 393 394 int childWidthSpec; 395 final int horizontalMargin = lp.leftMargin + lp.rightMargin; 396 if (lp.width == LayoutParams.WRAP_CONTENT) { 397 childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize - horizontalMargin, 398 MeasureSpec.AT_MOST); 399 } else if (lp.width == LayoutParams.FILL_PARENT) { 400 childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize - horizontalMargin, 401 MeasureSpec.EXACTLY); 402 } else { 403 childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); 404 } 405 406 int childHeightSpec; 407 if (lp.height == LayoutParams.WRAP_CONTENT) { 408 childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.AT_MOST); 409 } else if (lp.height == LayoutParams.FILL_PARENT) { 410 childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.EXACTLY); 411 } else { 412 childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 413 } 414 415 child.measure(childWidthSpec, childHeightSpec); 416 final int childWidth = child.getMeasuredWidth(); 417 final int childHeight = child.getMeasuredHeight(); 418 419 if (heightMode == MeasureSpec.AT_MOST && childHeight > layoutHeight) { 420 layoutHeight = Math.min(childHeight, maxLayoutHeight); 421 } 422 423 widthRemaining -= childWidth; 424 canSlide |= lp.slideable = widthRemaining < 0; 425 if (lp.slideable) { 426 mSlideableView = child; 427 } 428 } 429 430 // Resolve weight and make sure non-sliding panels are smaller than the full screen. 431 if (canSlide || weightSum > 0) { 432 final int fixedPanelWidthLimit = widthSize - mOverhangSize; 433 434 for (int i = 0; i < childCount; i++) { 435 final View child = getChildAt(i); 436 437 if (child.getVisibility() == GONE) { 438 continue; 439 } 440 441 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 442 443 final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0; 444 final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth(); 445 if (canSlide && child != mSlideableView) { 446 if (lp.width < 0 && (measuredWidth > fixedPanelWidthLimit || lp.weight > 0)) { 447 // Fixed panels in a sliding configuration should 448 // be clamped to the fixed panel limit. 449 final int childHeightSpec; 450 if (skippedFirstPass) { 451 // Do initial height measurement if we skipped measuring this view 452 // the first time around. 453 if (lp.height == LayoutParams.WRAP_CONTENT) { 454 childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, 455 MeasureSpec.AT_MOST); 456 } else if (lp.height == LayoutParams.FILL_PARENT) { 457 childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, 458 MeasureSpec.EXACTLY); 459 } else { 460 childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, 461 MeasureSpec.EXACTLY); 462 } 463 } else { 464 childHeightSpec = MeasureSpec.makeMeasureSpec( 465 child.getMeasuredHeight(), MeasureSpec.EXACTLY); 466 } 467 final int childWidthSpec = MeasureSpec.makeMeasureSpec( 468 fixedPanelWidthLimit, MeasureSpec.EXACTLY); 469 child.measure(childWidthSpec, childHeightSpec); 470 } 471 } else if (lp.weight > 0) { 472 int childHeightSpec; 473 if (lp.width == 0) { 474 // This was skipped the first time; figure out a real height spec. 475 if (lp.height == LayoutParams.WRAP_CONTENT) { 476 childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, 477 MeasureSpec.AT_MOST); 478 } else if (lp.height == LayoutParams.FILL_PARENT) { 479 childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, 480 MeasureSpec.EXACTLY); 481 } else { 482 childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, 483 MeasureSpec.EXACTLY); 484 } 485 } else { 486 childHeightSpec = MeasureSpec.makeMeasureSpec( 487 child.getMeasuredHeight(), MeasureSpec.EXACTLY); 488 } 489 490 if (canSlide) { 491 // Consume available space 492 final int horizontalMargin = lp.leftMargin + lp.rightMargin; 493 final int newWidth = widthSize - horizontalMargin; 494 final int childWidthSpec = MeasureSpec.makeMeasureSpec( 495 newWidth, MeasureSpec.EXACTLY); 496 if (measuredWidth != newWidth) { 497 child.measure(childWidthSpec, childHeightSpec); 498 } 499 } else { 500 // Distribute the extra width proportionally similar to LinearLayout 501 final int widthToDistribute = Math.max(0, widthRemaining); 502 final int addedWidth = (int) (lp.weight * widthToDistribute / weightSum); 503 final int childWidthSpec = MeasureSpec.makeMeasureSpec( 504 measuredWidth + addedWidth, MeasureSpec.EXACTLY); 505 child.measure(childWidthSpec, childHeightSpec); 506 } 507 } 508 } 509 } 510 511 setMeasuredDimension(widthSize, layoutHeight); 512 mCanSlide = canSlide; 513 if (mScrollState != SCROLL_STATE_IDLE && !canSlide) { 514 // Cancel scrolling in progress, it's no longer relevant. 515 setScrollState(SCROLL_STATE_IDLE); 516 } 517 } 518 519 @Override 520 protected void onLayout(boolean changed, int l, int t, int r, int b) { 521 final int width = r - l; 522 final int height = b - t; 523 final int paddingLeft = getPaddingLeft(); 524 final int paddingRight = getPaddingRight(); 525 final int paddingTop = getPaddingTop(); 526 final int paddingBottom = getPaddingBottom(); 527 528 final int childCount = getChildCount(); 529 int xStart = paddingLeft; 530 int nextXStart = xStart; 531 532 for (int i = 0; i < childCount; i++) { 533 final View child = getChildAt(i); 534 535 if (child.getVisibility() == GONE) { 536 continue; 537 } 538 539 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 540 541 final int childWidth = child.getMeasuredWidth(); 542 int offset = 0; 543 544 if (lp.slideable) { 545 final int margin = lp.leftMargin + lp.rightMargin; 546 final int range = Math.min(nextXStart, 547 width - paddingRight - mOverhangSize) - xStart - margin; 548 mSlideRange = range; 549 lp.dimWhenOffset = width - paddingRight - (xStart + range) < childWidth / 2; 550 xStart += (int) (range * mSlideOffset) + lp.leftMargin; 551 } else if (mCanSlide && mParallaxBy != 0) { 552 offset = (int) ((1 - mSlideOffset) * mParallaxBy); 553 xStart = nextXStart; 554 } else { 555 xStart = nextXStart; 556 } 557 558 final int childLeft = xStart - offset; 559 final int childRight = childLeft + childWidth; 560 final int childTop = paddingTop; 561 final int childBottom = childTop + child.getMeasuredHeight(); 562 child.layout(childLeft, paddingTop, childRight, childBottom); 563 564 nextXStart += child.getWidth(); 565 } 566 } 567 568 @Override 569 public boolean onInterceptTouchEvent(MotionEvent ev) { 570 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 571 572 if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { 573 return super.onInterceptTouchEvent(ev); 574 } 575 576 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 577 mActivePointerId = INVALID_POINTER; 578 if (mVelocityTracker != null) { 579 mVelocityTracker.recycle(); 580 mVelocityTracker = null; 581 } 582 return false; 583 } 584 585 boolean interceptTap = false; 586 587 switch (action) { 588 case MotionEvent.ACTION_MOVE: { 589 final int activePointerId = mActivePointerId; 590 if (activePointerId == INVALID_POINTER) { 591 // No valid pointer = no valid drag. Ignore. 592 break; 593 } 594 595 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); 596 final float x = MotionEventCompat.getX(ev, pointerIndex); 597 final float dx = x - mLastMotionX; 598 final float xDiff = Math.abs(dx); 599 final float y = MotionEventCompat.getY(ev, pointerIndex); 600 final float yDiff = Math.abs(y - mLastMotionY); 601 602 if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && 603 canScroll(this, false, (int) dx, (int) x, (int) y)) { 604 mInitialMotionX = mLastMotionX = x; 605 mLastMotionY = y; 606 mIsUnableToDrag = true; 607 return false; 608 } 609 if (xDiff > mTouchSlop && xDiff > yDiff && isSlideablePaneUnder(x, y)) { 610 mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : 611 mInitialMotionX - mTouchSlop; 612 setScrollState(SCROLL_STATE_DRAGGING); 613 } else if (yDiff > mTouchSlop) { 614 mIsUnableToDrag = true; 615 } 616 if (mScrollState == SCROLL_STATE_DRAGGING && performDrag(x)) { 617 invalidate(); 618 } 619 break; 620 } 621 622 case MotionEvent.ACTION_DOWN: { 623 final float x = ev.getX(); 624 final float y = ev.getY(); 625 mIsUnableToDrag = false; 626 mInitialMotionX = x; 627 mInitialMotionY = y; 628 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 629 if (isSlideablePaneUnder(x, y)) { 630 mLastMotionX = x; 631 mLastMotionY = y; 632 if (mScrollState == SCROLL_STATE_SETTLING) { 633 // Start dragging immediately. "Catch" 634 setScrollState(SCROLL_STATE_DRAGGING); 635 } else if (isDimmed(mSlideableView)) { 636 interceptTap = true; 637 } 638 } 639 break; 640 } 641 642 case MotionEventCompat.ACTION_POINTER_UP: 643 onSecondaryPointerUp(ev); 644 break; 645 } 646 647 if (mVelocityTracker == null) { 648 mVelocityTracker = VelocityTracker.obtain(); 649 } 650 mVelocityTracker.addMovement(ev); 651 return mScrollState == SCROLL_STATE_DRAGGING || interceptTap; 652 } 653 654 @Override 655 public boolean onTouchEvent(MotionEvent ev) { 656 if (!mCanSlide) { 657 return super.onTouchEvent(ev); 658 } 659 660 if (mVelocityTracker == null) { 661 mVelocityTracker = VelocityTracker.obtain(); 662 } 663 mVelocityTracker.addMovement(ev); 664 665 final int action = ev.getAction(); 666 boolean needsInvalidate = false; 667 boolean wantTouchEvents = true; 668 669 switch (action & MotionEventCompat.ACTION_MASK) { 670 case MotionEvent.ACTION_DOWN: { 671 final float x = ev.getX(); 672 final float y = ev.getY(); 673 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 674 mInitialMotionX = x; 675 mInitialMotionY = y; 676 677 if (isSlideablePaneUnder(x, y)) { 678 mScroller.abortAnimation(); 679 wantTouchEvents = true; 680 mLastMotionX = x; 681 setScrollState(SCROLL_STATE_DRAGGING); 682 } 683 break; 684 } 685 686 case MotionEvent.ACTION_MOVE: { 687 if (mScrollState != SCROLL_STATE_DRAGGING) { 688 final int pointerIndex = MotionEventCompat.findPointerIndex( 689 ev, mActivePointerId); 690 final float x = MotionEventCompat.getX(ev, pointerIndex); 691 final float y = MotionEventCompat.getY(ev, pointerIndex); 692 final float dx = Math.abs(x - mLastMotionX); 693 final float dy = Math.abs(y - mLastMotionY); 694 if (dx > mTouchSlop && dx > dy && isSlideablePaneUnder(x, y)) { 695 mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : 696 mInitialMotionX - mTouchSlop; 697 setScrollState(SCROLL_STATE_DRAGGING); 698 } 699 } 700 if (mScrollState == SCROLL_STATE_DRAGGING) { 701 final int activePointerIndex = MotionEventCompat.findPointerIndex( 702 ev, mActivePointerId); 703 final float x = MotionEventCompat.getX(ev, activePointerIndex); 704 needsInvalidate |= performDrag(x); 705 } 706 break; 707 } 708 709 case MotionEvent.ACTION_UP: { 710 if (isDimmed(mSlideableView)) { 711 final int pi = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 712 final float x = MotionEventCompat.getX(ev, pi); 713 final float y = MotionEventCompat.getY(ev, pi); 714 final float dx = x - mInitialMotionX; 715 final float dy = y - mInitialMotionY; 716 if (dx * dx + dy * dy < mTouchSlop * mTouchSlop && isSlideablePaneUnder(x, y)) { 717 // Taps close a dimmed open pane. 718 closePane(mSlideableView, 0); 719 mActivePointerId = INVALID_POINTER; 720 break; 721 } 722 } 723 if (mScrollState == SCROLL_STATE_DRAGGING) { 724 final VelocityTracker vt = mVelocityTracker; 725 vt.computeCurrentVelocity(1000, mMaxVelocity); 726 int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(vt, 727 mActivePointerId); 728 if (initialVelocity < 0 || (initialVelocity == 0 && mSlideOffset < 0.5f)) { 729 closePane(mSlideableView, initialVelocity); 730 } else { 731 openPane(mSlideableView, initialVelocity); 732 } 733 mActivePointerId = INVALID_POINTER; 734 } 735 break; 736 } 737 738 case MotionEvent.ACTION_CANCEL: { 739 if (mScrollState == SCROLL_STATE_DRAGGING) { 740 mActivePointerId = INVALID_POINTER; 741 if (mSlideOffset < 0.5f) { 742 closePane(mSlideableView, 0); 743 } else { 744 openPane(mSlideableView, 0); 745 } 746 } 747 break; 748 } 749 750 case MotionEventCompat.ACTION_POINTER_DOWN: { 751 final int index = MotionEventCompat.getActionIndex(ev); 752 mLastMotionX = MotionEventCompat.getX(ev, index); 753 mLastMotionY = MotionEventCompat.getY(ev, index); 754 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 755 break; 756 } 757 758 case MotionEventCompat.ACTION_POINTER_UP: { 759 onSecondaryPointerUp(ev); 760 break; 761 } 762 } 763 764 if (needsInvalidate) { 765 invalidate(); 766 } 767 return wantTouchEvents; 768 } 769 770 private void closePane(View pane, int initialVelocity) { 771 if (mCanSlide) { 772 smoothSlideTo(0.f, initialVelocity); 773 } 774 } 775 776 private void openPane(View pane, int initialVelocity) { 777 if (mCanSlide) { 778 smoothSlideTo(1.f, initialVelocity); 779 } 780 } 781 782 /** 783 * Animate the sliding panel to its open state. 784 */ 785 public void smoothSlideOpen() { 786 if (mCanSlide) { 787 openPane(mSlideableView, 0); 788 } 789 } 790 791 /** 792 * Animate the sliding panel to its closed state. 793 */ 794 public void smoothSlideClosed() { 795 if (mCanSlide) { 796 closePane(mSlideableView, 0); 797 } 798 } 799 800 /** 801 * @return true if sliding panels are completely open 802 */ 803 public boolean isOpen() { 804 return !mCanSlide || mSlideOffset == 1; 805 } 806 807 /** 808 * @return true if content in this layout can be slid open and closed 809 */ 810 public boolean canSlide() { 811 return mCanSlide; 812 } 813 814 private boolean performDrag(float x) { 815 final float dxMotion = x - mLastMotionX; 816 mLastMotionX = x; 817 818 final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); 819 final int leftBound = getPaddingLeft() + lp.leftMargin; 820 final int rightBound = leftBound + mSlideRange; 821 822 final float oldLeft = mSlideableView.getLeft(); 823 final float newLeft = Math.min(Math.max(oldLeft + dxMotion, leftBound), rightBound); 824 825 final int dxPane = (int) (newLeft - oldLeft); 826 827 if (dxPane == 0) { 828 return false; 829 } 830 831 mSlideableView.offsetLeftAndRight(dxPane); 832 833 mSlideOffset = (newLeft - leftBound) / mSlideRange; 834 835 if (mParallaxBy != 0) { 836 parallaxOtherViews(mSlideOffset); 837 } 838 839 mLastMotionX += newLeft - (int) newLeft; 840 if (lp.dimWhenOffset) { 841 dimChildView(mSlideableView, mSlideOffset); 842 } 843 dispatchOnPanelSlide(mSlideableView); 844 845 return true; 846 } 847 848 private void dimChildView(View v, float mag) { 849 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 850 851 if (mag > 0) { 852 final int baseAlpha = (DEFAULT_FADE_COLOR & 0xff000000) >>> 24; 853 int imag = (int) (baseAlpha * mag); 854 int color = imag << 24 | (DEFAULT_FADE_COLOR & 0xffffff); 855 if (lp.dimPaint == null) { 856 lp.dimPaint = new Paint(); 857 } 858 lp.dimPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_OVER)); 859 if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_HARDWARE) { 860 ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_HARDWARE, lp.dimPaint); 861 } 862 invalidateChildRegion(v); 863 } else if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_NONE) { 864 ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_NONE, null); 865 } 866 } 867 868 @Override 869 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 870 if (Build.VERSION.SDK_INT >= 11) { // HC 871 return super.drawChild(canvas, child, drawingTime); 872 } 873 874 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 875 if (lp.dimWhenOffset && mSlideOffset > 0) { 876 if (!child.isDrawingCacheEnabled()) { 877 child.setDrawingCacheEnabled(true); 878 } 879 final Bitmap cache = child.getDrawingCache(); 880 canvas.drawBitmap(cache, child.getLeft(), child.getTop(), lp.dimPaint); 881 return false; 882 } else { 883 if (child.isDrawingCacheEnabled()) { 884 child.setDrawingCacheEnabled(false); 885 } 886 return super.drawChild(canvas, child, drawingTime); 887 } 888 } 889 890 private void invalidateChildRegion(View v) { 891 IMPL.invalidateChildRegion(this, v); 892 } 893 894 private boolean isGutterDrag(float x, float dx) { 895 return (x < mGutterSize && dx > 0) || (x > getWidth() - mGutterSize && dx < 0); 896 } 897 898 /** 899 * Smoothly animate mDraggingPane to the target X position within its range. 900 * 901 * @param slideOffset position to animate to 902 * @param velocity initial velocity in case of fling, or 0. 903 */ 904 void smoothSlideTo(float slideOffset, int velocity) { 905 if (!mCanSlide) { 906 // Nothing to do. 907 return; 908 } 909 910 final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); 911 912 final int leftBound = getPaddingLeft() + lp.leftMargin; 913 int sx = mSlideableView.getLeft(); 914 int x = (int) (leftBound + slideOffset * mSlideRange); 915 int dx = x - sx; 916 if (dx == 0) { 917 setScrollState(SCROLL_STATE_IDLE); 918 if (mSlideOffset == 0) { 919 dispatchOnPanelClosed(mSlideableView); 920 } else { 921 dispatchOnPanelOpened(mSlideableView); 922 } 923 return; 924 } 925 926 setScrollState(SCROLL_STATE_SETTLING); 927 928 final int width = getWidth(); 929 final int halfWidth = width / 2; 930 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); 931 final float distance = halfWidth + halfWidth * 932 distanceInfluenceForSnapDuration(distanceRatio); 933 934 int duration = 0; 935 velocity = Math.abs(velocity); 936 if (velocity > 0) { 937 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 938 } else { 939 final float range = (float) Math.abs(dx) / mSlideRange; 940 duration = (int) ((range + 1) * 100); 941 } 942 duration = Math.min(duration, MAX_SETTLE_DURATION); 943 944 mScroller.startScroll(sx, 0, dx, 0, duration); 945 ViewCompat.postInvalidateOnAnimation(this); 946 } 947 948 // We want the duration of the page snap animation to be influenced by the distance that 949 // the screen has to travel, however, we don't want this duration to be effected in a 950 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 951 // of travel has on the overall snap duration. 952 float distanceInfluenceForSnapDuration(float f) { 953 f -= 0.5f; // center the values about 0. 954 f *= 0.3f * Math.PI / 2.0f; 955 return (float) Math.sin(f); 956 } 957 958 @Override 959 public void computeScroll() { 960 if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { 961 if (!mCanSlide) { 962 mScroller.abortAnimation(); 963 return; 964 } 965 966 final int oldLeft = mSlideableView.getLeft(); 967 final int newLeft = mScroller.getCurrX(); 968 final int dx = newLeft - oldLeft; 969 mSlideableView.offsetLeftAndRight(dx); 970 971 final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); 972 final int leftBound = getPaddingLeft() + lp.leftMargin; 973 mSlideOffset = (float) (newLeft - leftBound) / mSlideRange; 974 if (lp.dimWhenOffset) { 975 dimChildView(mSlideableView, mSlideOffset); 976 } 977 dispatchOnPanelSlide(mSlideableView); 978 979 if (mParallaxBy != 0) { 980 parallaxOtherViews(mSlideOffset); 981 } 982 983 if (mScroller.isFinished()) { 984 setScrollState(SCROLL_STATE_IDLE); 985 post(new Runnable() { 986 public void run() { 987 if (mSlideOffset == 0) { 988 dispatchOnPanelClosed(mSlideableView); 989 } else { 990 dispatchOnPanelOpened(mSlideableView); 991 } 992 } 993 }); 994 } 995 ViewCompat.postInvalidateOnAnimation(this); 996 } 997 998 } 999 1000 /** 1001 * Set a drawable to use as a shadow cast by the right pane onto the left pane 1002 * during opening/closing. 1003 * 1004 * @param d drawable to use as a shadow 1005 */ 1006 public void setShadowDrawable(Drawable d) { 1007 mShadowDrawable = d; 1008 } 1009 1010 /** 1011 * Set a drawable to use as a shadow cast by the right pane onto the left pane 1012 * during opening/closing. 1013 * 1014 * @param resId Resource ID of a drawable to use 1015 */ 1016 public void setShadowResource(int resId) { 1017 setShadowDrawable(getResources().getDrawable(resId)); 1018 } 1019 1020 @Override 1021 public void draw(Canvas c) { 1022 super.draw(c); 1023 1024 if (mSlideableView == null || mSlideableView.getVisibility() == GONE || 1025 mShadowDrawable == null) { 1026 // No need to draw a shadow if we don't have one. 1027 return; 1028 } 1029 1030 final int shadowWidth = mShadowDrawable.getIntrinsicWidth(); 1031 final int right = mSlideableView.getLeft(); 1032 final int top = mSlideableView.getTop(); 1033 final int bottom = mSlideableView.getBottom(); 1034 final int left = right - shadowWidth; 1035 mShadowDrawable.setBounds(left, top, right, bottom); 1036 mShadowDrawable.draw(c); 1037 } 1038 1039 private void parallaxOtherViews(float slideOffset) { 1040 final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams(); 1041 final boolean dimViews = slideLp.dimWhenOffset && slideLp.leftMargin <= 0; 1042 final int childCount = getChildCount(); 1043 for (int i = 0; i < childCount; i++) { 1044 final View v = getChildAt(i); 1045 if (v == mSlideableView) continue; 1046 1047 final int oldOffset = (int) ((1 - mParallaxOffset) * mParallaxBy); 1048 mParallaxOffset = slideOffset; 1049 final int newOffset = (int) ((1 - slideOffset) * mParallaxBy); 1050 final int dx = oldOffset - newOffset; 1051 1052 v.offsetLeftAndRight(dx); 1053 1054 if (dimViews) { 1055 dimChildView(v, 1 - mParallaxOffset); 1056 } 1057 } 1058 } 1059 1060 /** 1061 * Tests scrollability within child views of v given a delta of dx. 1062 * 1063 * @param v View to test for horizontal scrollability 1064 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 1065 * or just its children (false). 1066 * @param dx Delta scrolled in pixels 1067 * @param x X coordinate of the active touch point 1068 * @param y Y coordinate of the active touch point 1069 * @return true if child views of v can be scrolled by delta of dx. 1070 */ 1071 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 1072 if (v instanceof ViewGroup) { 1073 final ViewGroup group = (ViewGroup) v; 1074 final int scrollX = v.getScrollX(); 1075 final int scrollY = v.getScrollY(); 1076 final int count = group.getChildCount(); 1077 // Count backwards - let topmost views consume scroll distance first. 1078 for (int i = count - 1; i >= 0; i--) { 1079 // TODO: Add versioned support here for transformed views. 1080 // This will not work for transformed views in Honeycomb+ 1081 final View child = group.getChildAt(i); 1082 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 1083 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 1084 canScroll(child, true, dx, x + scrollX - child.getLeft(), 1085 y + scrollY - child.getTop())) { 1086 return true; 1087 } 1088 } 1089 } 1090 1091 return checkV && ViewCompat.canScrollHorizontally(v, -dx); 1092 } 1093 1094 private void onSecondaryPointerUp(MotionEvent ev) { 1095 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 1096 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 1097 if (pointerId == mActivePointerId) { 1098 // This was our active pointer going up. Choose a new 1099 // active pointer and adjust accordingly. 1100 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 1101 mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 1102 mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex); 1103 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 1104 if (mVelocityTracker != null) { 1105 mVelocityTracker.clear(); 1106 } 1107 } 1108 } 1109 1110 boolean isSlideablePaneUnder(float x, float y) { 1111 final View child = mSlideableView; 1112 return child != null && 1113 x >= child.getLeft() - mGutterSize && 1114 x < child.getRight() + mGutterSize && 1115 y >= child.getTop() && 1116 y < child.getBottom(); 1117 } 1118 1119 boolean isDimmed(View child) { 1120 if (child == null) { 1121 return false; 1122 } 1123 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1124 return mCanSlide && lp.dimWhenOffset && mSlideOffset > 0; 1125 } 1126 1127 @Override 1128 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 1129 return new LayoutParams(); 1130 } 1131 1132 @Override 1133 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1134 return p instanceof MarginLayoutParams 1135 ? new LayoutParams((MarginLayoutParams) p) 1136 : new LayoutParams(p); 1137 } 1138 1139 @Override 1140 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1141 return p instanceof LayoutParams && super.checkLayoutParams(p); 1142 } 1143 1144 @Override 1145 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1146 return new LayoutParams(getContext(), attrs); 1147 } 1148 1149 public static class LayoutParams extends ViewGroup.MarginLayoutParams { 1150 private static final int[] ATTRS = new int[] { 1151 android.R.attr.layout_weight 1152 }; 1153 1154 /** 1155 * The weighted proportion of how much of the leftover space 1156 * this child should consume after measurement. 1157 */ 1158 public float weight = 0; 1159 1160 /** 1161 * True if this pane is the slideable pane in the layout. 1162 */ 1163 boolean slideable; 1164 1165 /** 1166 * True if this view should be drawn dimmed 1167 * when it's been offset from its default position. 1168 */ 1169 boolean dimWhenOffset; 1170 1171 Paint dimPaint; 1172 1173 public LayoutParams() { 1174 super(FILL_PARENT, FILL_PARENT); 1175 } 1176 1177 public LayoutParams(int width, int height) { 1178 super(width, height); 1179 } 1180 1181 public LayoutParams(android.view.ViewGroup.LayoutParams source) { 1182 super(source); 1183 } 1184 1185 public LayoutParams(MarginLayoutParams source) { 1186 super(source); 1187 } 1188 1189 public LayoutParams(LayoutParams source) { 1190 super(source); 1191 this.weight = source.weight; 1192 } 1193 1194 public LayoutParams(Context c, AttributeSet attrs) { 1195 super(c, attrs); 1196 1197 final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); 1198 this.weight = a.getFloat(0, 0); 1199 a.recycle(); 1200 } 1201 1202 } 1203 1204 static class SavedState extends BaseSavedState { 1205 boolean canSlide; 1206 boolean isOpen; 1207 1208 SavedState(Parcelable superState) { 1209 super(superState); 1210 } 1211 1212 private SavedState(Parcel in) { 1213 super(in); 1214 canSlide = in.readInt() != 0; 1215 isOpen = in.readInt() != 0; 1216 } 1217 1218 @Override 1219 public void writeToParcel(Parcel out, int flags) { 1220 super.writeToParcel(out, flags); 1221 out.writeInt(canSlide ? 1 : 0); 1222 out.writeInt(isOpen ? 1 : 0); 1223 } 1224 1225 public static final Parcelable.Creator<SavedState> CREATOR = 1226 new Parcelable.Creator<SavedState>() { 1227 public SavedState createFromParcel(Parcel in) { 1228 return new SavedState(in); 1229 } 1230 1231 public SavedState[] newArray(int size) { 1232 return new SavedState[size]; 1233 } 1234 }; 1235 } 1236 1237 interface SlidingPanelLayoutImpl { 1238 void invalidateChildRegion(SlidingPaneLayout parent, View child); 1239 } 1240 1241 static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl { 1242 public void invalidateChildRegion(SlidingPaneLayout parent, View child) { 1243 ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(), 1244 child.getRight(), child.getBottom()); 1245 } 1246 } 1247 1248 static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase { 1249 /* 1250 * Private API hacks! Nasty! Bad! 1251 * 1252 * In Jellybean, some optimizations in the hardware UI renderer 1253 * prevent a changed Paint on a View using a hardware layer from having 1254 * the intended effect. This twiddles some internal bits on the view to force 1255 * it to recreate the display list. 1256 */ 1257 private Method mGetDisplayList; 1258 private Field mRecreateDisplayList; 1259 1260 SlidingPanelLayoutImplJB() { 1261 try { 1262 mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null); 1263 } catch (NoSuchMethodException e) { 1264 Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.", e); 1265 } 1266 try { 1267 mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList"); 1268 mRecreateDisplayList.setAccessible(true); 1269 } catch (NoSuchFieldException e) { 1270 Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.", e); 1271 } 1272 } 1273 1274 @Override 1275 public void invalidateChildRegion(SlidingPaneLayout parent, View child) { 1276 if (mGetDisplayList != null && mRecreateDisplayList != null) { 1277 try { 1278 mRecreateDisplayList.setBoolean(child, true); 1279 mGetDisplayList.invoke(child, (Object[]) null); 1280 } catch (Exception e) { 1281 Log.e(TAG, "Error refreshing display list state", e); 1282 } 1283 } else { 1284 // Slow path. REALLY slow path. Let's hope we don't get here. 1285 child.invalidate(); 1286 return; 1287 } 1288 super.invalidateChildRegion(parent, child); 1289 } 1290 } 1291 1292 static class SlidingPanelLayoutImplJBMR1 extends SlidingPanelLayoutImplBase { 1293 @Override 1294 public void invalidateChildRegion(SlidingPaneLayout parent, View child) { 1295 ViewCompat.setLayerPaint(child, ((LayoutParams) child.getLayoutParams()).dimPaint); 1296 } 1297 } 1298} 1299