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