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