SlidingPaneLayout.java revision 3f50aafe43ea3c7d1c09fbd7afa87bf7453beb18
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 = xStart + lp.leftMargin + range + childWidth / 2 > 564 width - paddingRight; 565 xStart += (int) (range * mSlideOffset) + lp.leftMargin; 566 } else if (mCanSlide && mParallaxBy != 0) { 567 offset = (int) ((1 - mSlideOffset) * mParallaxBy); 568 xStart = nextXStart; 569 } else { 570 xStart = nextXStart; 571 } 572 573 final int childLeft = xStart - offset; 574 final int childRight = childLeft + childWidth; 575 final int childTop = paddingTop; 576 final int childBottom = childTop + child.getMeasuredHeight(); 577 child.layout(childLeft, paddingTop, childRight, childBottom); 578 579 nextXStart += child.getWidth(); 580 } 581 } 582 583 @Override 584 public boolean onInterceptTouchEvent(MotionEvent ev) { 585 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 586 587 if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { 588 return super.onInterceptTouchEvent(ev); 589 } 590 591 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { 592 mActivePointerId = INVALID_POINTER; 593 return false; 594 } 595 596 final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); 597 598 boolean interceptTap = false; 599 600 switch (action) { 601 case MotionEvent.ACTION_DOWN: 602 mIsUnableToDrag = false; 603 case MotionEventCompat.ACTION_POINTER_DOWN: { 604 final int index = action == MotionEvent.ACTION_DOWN ? 605 0 : ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK; 606 final float x = MotionEventCompat.getX(ev, index); 607 final float y = MotionEventCompat.getY(ev, index); 608 mInitialMotionX = x; 609 mInitialMotionY = y; 610 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 611 if (isSlideablePaneUnder(x, y) && mScrollState != SCROLL_STATE_SETTLING && 612 isDimmed(mSlideableView)) { 613 interceptTap = true; 614 } 615 break; 616 } 617 618 case MotionEventCompat.ACTION_POINTER_UP: 619 onSecondaryPointerUp(ev); 620 break; 621 } 622 623 return interceptForDrag || interceptTap; 624 } 625 626 @Override 627 public boolean onTouchEvent(MotionEvent ev) { 628 if (!mCanSlide) { 629 return super.onTouchEvent(ev); 630 } 631 632 mDragHelper.processTouchEvent(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 mInitialMotionX = x; 644 mInitialMotionY = y; 645 break; 646 } 647 648 case MotionEvent.ACTION_UP: { 649 if (isDimmed(mSlideableView)) { 650 final int pi = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 651 final float x = MotionEventCompat.getX(ev, pi); 652 final float y = MotionEventCompat.getY(ev, pi); 653 final float dx = x - mInitialMotionX; 654 final float dy = y - mInitialMotionY; 655 if (dx * dx + dy * dy < mTouchSlop * mTouchSlop && isSlideablePaneUnder(x, y)) { 656 // Taps close a dimmed open pane. 657 closePane(mSlideableView, 0); 658 mActivePointerId = INVALID_POINTER; 659 break; 660 } 661 } 662 break; 663 } 664 665 case MotionEvent.ACTION_CANCEL: { 666 if (mScrollState == SCROLL_STATE_DRAGGING) { 667 mActivePointerId = INVALID_POINTER; 668 if (mSlideOffset < 0.5f) { 669 closePane(mSlideableView, 0); 670 } else { 671 openPane(mSlideableView, 0); 672 } 673 } 674 break; 675 } 676 677 case MotionEventCompat.ACTION_POINTER_DOWN: { 678 final int index = MotionEventCompat.getActionIndex(ev); 679 mLastMotionX = MotionEventCompat.getX(ev, index); 680 mLastMotionY = MotionEventCompat.getY(ev, index); 681 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 682 break; 683 } 684 685 case MotionEventCompat.ACTION_POINTER_UP: { 686 onSecondaryPointerUp(ev); 687 break; 688 } 689 } 690 691 if (needsInvalidate) { 692 invalidate(); 693 } 694 return wantTouchEvents; 695 } 696 697 private void closePane(View pane, int initialVelocity) { 698 if (mCanSlide) { 699 smoothSlideTo(0.f, initialVelocity); 700 } 701 } 702 703 private void openPane(View pane, int initialVelocity) { 704 if (mCanSlide) { 705 smoothSlideTo(1.f, initialVelocity); 706 } 707 } 708 709 /** 710 * Animate the sliding panel to its open state. 711 */ 712 public void smoothSlideOpen() { 713 if (mCanSlide) { 714 openPane(mSlideableView, 0); 715 } 716 } 717 718 /** 719 * Animate the sliding panel to its closed state. 720 */ 721 public void smoothSlideClosed() { 722 if (mCanSlide) { 723 closePane(mSlideableView, 0); 724 } 725 } 726 727 /** 728 * @return true if sliding panels are completely open 729 */ 730 public boolean isOpen() { 731 return !mCanSlide || mSlideOffset == 1; 732 } 733 734 /** 735 * @return true if content in this layout can be slid open and closed 736 */ 737 public boolean canSlide() { 738 return mCanSlide; 739 } 740 741 private boolean performDrag(int newLeft) { 742 final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); 743 final int leftBound = getPaddingLeft() + lp.leftMargin; 744 745 final float oldLeft = mSlideableView.getLeft(); 746 747 final int dxPane = (int) (newLeft - oldLeft); 748 749 mSlideableView.offsetLeftAndRight(dxPane); 750 751 mSlideOffset = (float) (newLeft - leftBound) / mSlideRange; 752 753 if (mParallaxBy != 0) { 754 parallaxOtherViews(mSlideOffset); 755 } 756 757 if (lp.dimWhenOffset) { 758 dimChildView(mSlideableView, mSlideOffset, mSliderFadeColor); 759 } 760 dispatchOnPanelSlide(mSlideableView); 761 762 return true; 763 } 764 765 private void dimChildView(View v, float mag, int fadeColor) { 766 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 767 768 if (mag > 0 && fadeColor != 0) { 769 final int baseAlpha = (fadeColor & 0xff000000) >>> 24; 770 int imag = (int) (baseAlpha * mag); 771 int color = imag << 24 | (fadeColor & 0xffffff); 772 if (lp.dimPaint == null) { 773 lp.dimPaint = new Paint(); 774 } 775 lp.dimPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_OVER)); 776 if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_HARDWARE) { 777 ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_HARDWARE, lp.dimPaint); 778 } 779 invalidateChildRegion(v); 780 } else if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_NONE) { 781 ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_NONE, null); 782 } 783 } 784 785 @Override 786 protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 787 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 788 boolean result; 789 final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); 790 791 if (mCanSlide && !lp.slideable && mSlideableView != null) { 792 // Clip against the slider; no sense drawing what will immediately be covered. 793 canvas.getClipBounds(mTmpRect); 794 mTmpRect.right = Math.min(mTmpRect.right, mSlideableView.getLeft()); 795 canvas.clipRect(mTmpRect); 796 } 797 798 if (Build.VERSION.SDK_INT >= 11) { // HC 799 result = super.drawChild(canvas, child, drawingTime); 800 } else { 801 if (lp.dimWhenOffset && mSlideOffset > 0) { 802 if (!child.isDrawingCacheEnabled()) { 803 child.setDrawingCacheEnabled(true); 804 } 805 final Bitmap cache = child.getDrawingCache(); 806 canvas.drawBitmap(cache, child.getLeft(), child.getTop(), lp.dimPaint); 807 result = false; 808 } else { 809 if (child.isDrawingCacheEnabled()) { 810 child.setDrawingCacheEnabled(false); 811 } 812 result = super.drawChild(canvas, child, drawingTime); 813 } 814 } 815 816 canvas.restoreToCount(save); 817 818 return result; 819 } 820 821 private void invalidateChildRegion(View v) { 822 IMPL.invalidateChildRegion(this, v); 823 } 824 825 /** 826 * Smoothly animate mDraggingPane to the target X position within its range. 827 * 828 * @param slideOffset position to animate to 829 * @param velocity initial velocity in case of fling, or 0. 830 */ 831 void smoothSlideTo(float slideOffset, int velocity) { 832 if (!mCanSlide) { 833 // Nothing to do. 834 return; 835 } 836 837 final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); 838 839 final int leftBound = getPaddingLeft() + lp.leftMargin; 840 int x = (int) (leftBound + slideOffset * mSlideRange); 841 842 if (mDragHelper.smoothSlideViewTo(mSlideableView, x, mSlideableView.getTop())) { 843 ViewCompat.postInvalidateOnAnimation(this); 844 } 845 } 846 847 // We want the duration of the page snap animation to be influenced by the distance that 848 // the screen has to travel, however, we don't want this duration to be effected in a 849 // purely linear fashion. Instead, we use this method to moderate the effect that the distance 850 // of travel has on the overall snap duration. 851 float distanceInfluenceForSnapDuration(float f) { 852 f -= 0.5f; // center the values about 0. 853 f *= 0.3f * Math.PI / 2.0f; 854 return (float) Math.sin(f); 855 } 856 857 @Override 858 public void computeScroll() { 859 if (mDragHelper.continueSettling(true)) { 860 if (!mCanSlide) { 861 mDragHelper.abort(); 862 return; 863 } 864 865 ViewCompat.postInvalidateOnAnimation(this); 866 } 867 } 868 869 /** 870 * Set a drawable to use as a shadow cast by the right pane onto the left pane 871 * during opening/closing. 872 * 873 * @param d drawable to use as a shadow 874 */ 875 public void setShadowDrawable(Drawable d) { 876 mShadowDrawable = d; 877 } 878 879 /** 880 * Set a drawable to use as a shadow cast by the right pane onto the left pane 881 * during opening/closing. 882 * 883 * @param resId Resource ID of a drawable to use 884 */ 885 public void setShadowResource(int resId) { 886 setShadowDrawable(getResources().getDrawable(resId)); 887 } 888 889 @Override 890 public void draw(Canvas c) { 891 super.draw(c); 892 893 final View shadowView = getChildCount() > 1 ? getChildAt(1) : null; 894 if (shadowView == null || mShadowDrawable == null) { 895 // No need to draw a shadow if we don't have one. 896 return; 897 } 898 899 final int shadowWidth = mShadowDrawable.getIntrinsicWidth(); 900 final int right = shadowView.getLeft(); 901 final int top = shadowView.getTop(); 902 final int bottom = shadowView.getBottom(); 903 final int left = right - shadowWidth; 904 mShadowDrawable.setBounds(left, top, right, bottom); 905 mShadowDrawable.draw(c); 906 } 907 908 private void parallaxOtherViews(float slideOffset) { 909 final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams(); 910 final boolean dimViews = slideLp.dimWhenOffset && slideLp.leftMargin <= 0; 911 final int childCount = getChildCount(); 912 for (int i = 0; i < childCount; i++) { 913 final View v = getChildAt(i); 914 if (v == mSlideableView) continue; 915 916 final int oldOffset = (int) ((1 - mParallaxOffset) * mParallaxBy); 917 mParallaxOffset = slideOffset; 918 final int newOffset = (int) ((1 - slideOffset) * mParallaxBy); 919 final int dx = oldOffset - newOffset; 920 921 v.offsetLeftAndRight(dx); 922 923 if (dimViews) { 924 dimChildView(v, 1 - mParallaxOffset, mCoveredFadeColor); 925 } 926 } 927 } 928 929 /** 930 * Tests scrollability within child views of v given a delta of dx. 931 * 932 * @param v View to test for horizontal scrollability 933 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 934 * or just its children (false). 935 * @param dx Delta scrolled in pixels 936 * @param x X coordinate of the active touch point 937 * @param y Y coordinate of the active touch point 938 * @return true if child views of v can be scrolled by delta of dx. 939 */ 940 protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { 941 if (v instanceof ViewGroup) { 942 final ViewGroup group = (ViewGroup) v; 943 final int scrollX = v.getScrollX(); 944 final int scrollY = v.getScrollY(); 945 final int count = group.getChildCount(); 946 // Count backwards - let topmost views consume scroll distance first. 947 for (int i = count - 1; i >= 0; i--) { 948 // TODO: Add versioned support here for transformed views. 949 // This will not work for transformed views in Honeycomb+ 950 final View child = group.getChildAt(i); 951 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 952 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 953 canScroll(child, true, dx, x + scrollX - child.getLeft(), 954 y + scrollY - child.getTop())) { 955 return true; 956 } 957 } 958 } 959 960 return checkV && ViewCompat.canScrollHorizontally(v, -dx); 961 } 962 963 private void onSecondaryPointerUp(MotionEvent ev) { 964 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 965 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 966 if (pointerId == mActivePointerId) { 967 // This was our active pointer going up. Choose a new 968 // active pointer and adjust accordingly. 969 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 970 mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); 971 mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex); 972 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 973 } 974 } 975 976 boolean isSlideablePaneUnder(float x, float y) { 977 final View child = mSlideableView; 978 return child != null && 979 x >= child.getLeft() && 980 x < child.getRight() && 981 y >= child.getTop() && 982 y < child.getBottom(); 983 } 984 985 boolean isDimmed(View child) { 986 if (child == null) { 987 return false; 988 } 989 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 990 return mCanSlide && lp.dimWhenOffset && mSlideOffset > 0; 991 } 992 993 @Override 994 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 995 return new LayoutParams(); 996 } 997 998 @Override 999 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1000 return p instanceof MarginLayoutParams 1001 ? new LayoutParams((MarginLayoutParams) p) 1002 : new LayoutParams(p); 1003 } 1004 1005 @Override 1006 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1007 return p instanceof LayoutParams && super.checkLayoutParams(p); 1008 } 1009 1010 @Override 1011 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1012 return new LayoutParams(getContext(), attrs); 1013 } 1014 1015 private class DragHelperCallback extends ViewDragHelper.Callback { 1016 1017 @Override 1018 public boolean tryCaptureView(View child, int pointerId) { 1019 if (mIsUnableToDrag) { 1020 return false; 1021 } 1022 1023 return ((LayoutParams) child.getLayoutParams()).slideable; 1024 } 1025 1026 @Override 1027 public void onViewDragStateChanged(int state) { 1028 // All of the state values used here are set based on ViewDragHelper's values. 1029 setScrollState(state); 1030 1031 if (mSlideOffset == 0) { 1032 dispatchOnPanelClosed(mSlideableView); 1033 } else { 1034 dispatchOnPanelOpened(mSlideableView); 1035 } 1036 } 1037 1038 @Override 1039 public void onViewPositionChanged(int x, int y, int dx, int dy) { 1040 performDrag(x); 1041 invalidate(); 1042 } 1043 1044 @Override 1045 public void onViewCaptured(View capturedChild, int activePointerId) { 1046 super.onViewCaptured(capturedChild, activePointerId); 1047 } 1048 1049 @Override 1050 public void onViewReleased(View releasedChild, float xvel, float yvel) { 1051 final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); 1052 int left = getPaddingLeft() + lp.leftMargin; 1053 if (xvel > 0 || (xvel == 0 && mSlideOffset > 0.5f)) { 1054 left += mSlideRange; 1055 } 1056 mDragHelper.settleCapturedViewAt(left, releasedChild.getTop()); 1057 invalidate(); 1058 } 1059 1060 @Override 1061 public int getViewHorizontalDragRange(View child) { 1062 return mSlideRange; 1063 } 1064 1065 @Override 1066 public int clampViewMotionHorizontal(View child, int dx) { 1067 final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); 1068 final int leftBound = getPaddingLeft() + lp.leftMargin; 1069 final int rightBound = leftBound + mSlideRange; 1070 1071 final int oldLeft = mSlideableView.getLeft(); 1072 final int newLeft = Math.min(Math.max(oldLeft + dx, leftBound), rightBound); 1073 1074 final int dxPane = newLeft - oldLeft; 1075 1076 return dxPane; 1077 } 1078 1079 @Override 1080 public void onEdgeDragStarted(int edgeFlags, int pointerId) { 1081 mDragHelper.captureChildView(mSlideableView, pointerId); 1082 } 1083 } 1084 1085 public static class LayoutParams extends ViewGroup.MarginLayoutParams { 1086 private static final int[] ATTRS = new int[] { 1087 android.R.attr.layout_weight 1088 }; 1089 1090 /** 1091 * The weighted proportion of how much of the leftover space 1092 * this child should consume after measurement. 1093 */ 1094 public float weight = 0; 1095 1096 /** 1097 * True if this pane is the slideable pane in the layout. 1098 */ 1099 boolean slideable; 1100 1101 /** 1102 * True if this view should be drawn dimmed 1103 * when it's been offset from its default position. 1104 */ 1105 boolean dimWhenOffset; 1106 1107 Paint dimPaint; 1108 1109 public LayoutParams() { 1110 super(FILL_PARENT, FILL_PARENT); 1111 } 1112 1113 public LayoutParams(int width, int height) { 1114 super(width, height); 1115 } 1116 1117 public LayoutParams(android.view.ViewGroup.LayoutParams source) { 1118 super(source); 1119 } 1120 1121 public LayoutParams(MarginLayoutParams source) { 1122 super(source); 1123 } 1124 1125 public LayoutParams(LayoutParams source) { 1126 super(source); 1127 this.weight = source.weight; 1128 } 1129 1130 public LayoutParams(Context c, AttributeSet attrs) { 1131 super(c, attrs); 1132 1133 final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); 1134 this.weight = a.getFloat(0, 0); 1135 a.recycle(); 1136 } 1137 1138 } 1139 1140 static class SavedState extends BaseSavedState { 1141 boolean canSlide; 1142 boolean isOpen; 1143 1144 SavedState(Parcelable superState) { 1145 super(superState); 1146 } 1147 1148 private SavedState(Parcel in) { 1149 super(in); 1150 canSlide = in.readInt() != 0; 1151 isOpen = in.readInt() != 0; 1152 } 1153 1154 @Override 1155 public void writeToParcel(Parcel out, int flags) { 1156 super.writeToParcel(out, flags); 1157 out.writeInt(canSlide ? 1 : 0); 1158 out.writeInt(isOpen ? 1 : 0); 1159 } 1160 1161 public static final Parcelable.Creator<SavedState> CREATOR = 1162 new Parcelable.Creator<SavedState>() { 1163 public SavedState createFromParcel(Parcel in) { 1164 return new SavedState(in); 1165 } 1166 1167 public SavedState[] newArray(int size) { 1168 return new SavedState[size]; 1169 } 1170 }; 1171 } 1172 1173 interface SlidingPanelLayoutImpl { 1174 void invalidateChildRegion(SlidingPaneLayout parent, View child); 1175 } 1176 1177 static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl { 1178 public void invalidateChildRegion(SlidingPaneLayout parent, View child) { 1179 ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(), 1180 child.getRight(), child.getBottom()); 1181 } 1182 } 1183 1184 static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase { 1185 /* 1186 * Private API hacks! Nasty! Bad! 1187 * 1188 * In Jellybean, some optimizations in the hardware UI renderer 1189 * prevent a changed Paint on a View using a hardware layer from having 1190 * the intended effect. This twiddles some internal bits on the view to force 1191 * it to recreate the display list. 1192 */ 1193 private Method mGetDisplayList; 1194 private Field mRecreateDisplayList; 1195 1196 SlidingPanelLayoutImplJB() { 1197 try { 1198 mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null); 1199 } catch (NoSuchMethodException e) { 1200 Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.", e); 1201 } 1202 try { 1203 mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList"); 1204 mRecreateDisplayList.setAccessible(true); 1205 } catch (NoSuchFieldException e) { 1206 Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.", e); 1207 } 1208 } 1209 1210 @Override 1211 public void invalidateChildRegion(SlidingPaneLayout parent, View child) { 1212 if (mGetDisplayList != null && mRecreateDisplayList != null) { 1213 try { 1214 mRecreateDisplayList.setBoolean(child, true); 1215 mGetDisplayList.invoke(child, (Object[]) null); 1216 } catch (Exception e) { 1217 Log.e(TAG, "Error refreshing display list state", e); 1218 } 1219 } else { 1220 // Slow path. REALLY slow path. Let's hope we don't get here. 1221 child.invalidate(); 1222 return; 1223 } 1224 super.invalidateChildRegion(parent, child); 1225 } 1226 } 1227 1228 static class SlidingPanelLayoutImplJBMR1 extends SlidingPanelLayoutImplBase { 1229 @Override 1230 public void invalidateChildRegion(SlidingPaneLayout parent, View child) { 1231 ViewCompat.setLayerPaint(child, ((LayoutParams) child.getLayoutParams()).dimPaint); 1232 } 1233 } 1234} 1235