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