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