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