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