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