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