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