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