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