1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.support.design.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.os.Parcel; 22 import android.os.Parcelable; 23 import android.support.annotation.IntDef; 24 import android.support.annotation.NonNull; 25 import android.support.design.R; 26 import android.support.v4.os.ParcelableCompat; 27 import android.support.v4.os.ParcelableCompatCreatorCallbacks; 28 import android.support.v4.view.AbsSavedState; 29 import android.support.v4.view.MotionEventCompat; 30 import android.support.v4.view.NestedScrollingChild; 31 import android.support.v4.view.VelocityTrackerCompat; 32 import android.support.v4.view.ViewCompat; 33 import android.support.v4.widget.ViewDragHelper; 34 import android.util.AttributeSet; 35 import android.view.MotionEvent; 36 import android.view.VelocityTracker; 37 import android.view.View; 38 import android.view.ViewConfiguration; 39 import android.view.ViewGroup; 40 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.lang.ref.WeakReference; 44 45 46 /** 47 * An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as 48 * a bottom sheet. 49 */ 50 public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> { 51 52 /** 53 * Callback for monitoring events about bottom sheets. 54 */ 55 public abstract static class BottomSheetCallback { 56 57 /** 58 * Called when the bottom sheet changes its state. 59 * 60 * @param bottomSheet The bottom sheet view. 61 * @param newState The new state. This will be one of {@link #STATE_DRAGGING}, 62 * {@link #STATE_SETTLING}, {@link #STATE_EXPANDED}, 63 * {@link #STATE_COLLAPSED}, or {@link #STATE_HIDDEN}. 64 */ onStateChanged(@onNull View bottomSheet, @State int newState)65 public abstract void onStateChanged(@NonNull View bottomSheet, @State int newState); 66 67 /** 68 * Called when the bottom sheet is being dragged. 69 * 70 * @param bottomSheet The bottom sheet view. 71 * @param slideOffset The new offset of this bottom sheet within its range, from 0 to 1 72 * when it is moving upward, and from 0 to -1 when it moving downward. 73 */ onSlide(@onNull View bottomSheet, float slideOffset)74 public abstract void onSlide(@NonNull View bottomSheet, float slideOffset); 75 } 76 77 /** 78 * The bottom sheet is dragging. 79 */ 80 public static final int STATE_DRAGGING = 1; 81 82 /** 83 * The bottom sheet is settling. 84 */ 85 public static final int STATE_SETTLING = 2; 86 87 /** 88 * The bottom sheet is expanded. 89 */ 90 public static final int STATE_EXPANDED = 3; 91 92 /** 93 * The bottom sheet is collapsed. 94 */ 95 public static final int STATE_COLLAPSED = 4; 96 97 /** 98 * The bottom sheet is hidden. 99 */ 100 public static final int STATE_HIDDEN = 5; 101 102 /** @hide */ 103 @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN}) 104 @Retention(RetentionPolicy.SOURCE) 105 public @interface State {} 106 107 private static final float HIDE_THRESHOLD = 0.5f; 108 109 private static final float HIDE_FRICTION = 0.1f; 110 111 private float mMaximumVelocity; 112 113 private int mPeekHeight; 114 115 private int mMinOffset; 116 117 private int mMaxOffset; 118 119 private boolean mHideable; 120 121 private boolean mSkipCollapsed; 122 123 @State 124 private int mState = STATE_COLLAPSED; 125 126 private ViewDragHelper mViewDragHelper; 127 128 private boolean mIgnoreEvents; 129 130 private int mLastNestedScrollDy; 131 132 private boolean mNestedScrolled; 133 134 private int mParentHeight; 135 136 private WeakReference<V> mViewRef; 137 138 private WeakReference<View> mNestedScrollingChildRef; 139 140 private BottomSheetCallback mCallback; 141 142 private VelocityTracker mVelocityTracker; 143 144 private int mActivePointerId; 145 146 private int mInitialY; 147 148 private boolean mTouchingScrollingChild; 149 150 /** 151 * Default constructor for instantiating BottomSheetBehaviors. 152 */ BottomSheetBehavior()153 public BottomSheetBehavior() { 154 } 155 156 /** 157 * Default constructor for inflating BottomSheetBehaviors from layout. 158 * 159 * @param context The {@link Context}. 160 * @param attrs The {@link AttributeSet}. 161 */ BottomSheetBehavior(Context context, AttributeSet attrs)162 public BottomSheetBehavior(Context context, AttributeSet attrs) { 163 super(context, attrs); 164 TypedArray a = context.obtainStyledAttributes(attrs, 165 R.styleable.BottomSheetBehavior_Layout); 166 setPeekHeight(a.getDimensionPixelSize( 167 R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, 0)); 168 setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false)); 169 setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, 170 false)); 171 a.recycle(); 172 ViewConfiguration configuration = ViewConfiguration.get(context); 173 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 174 } 175 176 @Override onSaveInstanceState(CoordinatorLayout parent, V child)177 public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) { 178 return new SavedState(super.onSaveInstanceState(parent, child), mState); 179 } 180 181 @Override onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state)182 public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) { 183 SavedState ss = (SavedState) state; 184 super.onRestoreInstanceState(parent, child, ss.getSuperState()); 185 // Intermediate states are restored as collapsed state 186 if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { 187 mState = STATE_COLLAPSED; 188 } else { 189 mState = ss.state; 190 } 191 } 192 193 @Override onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)194 public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { 195 if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) { 196 ViewCompat.setFitsSystemWindows(child, true); 197 } 198 int savedTop = child.getTop(); 199 // First let the parent lay it out 200 parent.onLayoutChild(child, layoutDirection); 201 // Offset the bottom sheet 202 mParentHeight = parent.getHeight(); 203 mMinOffset = Math.max(0, mParentHeight - child.getHeight()); 204 mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset); 205 if (mState == STATE_EXPANDED) { 206 ViewCompat.offsetTopAndBottom(child, mMinOffset); 207 } else if (mHideable && mState == STATE_HIDDEN) { 208 ViewCompat.offsetTopAndBottom(child, mParentHeight); 209 } else if (mState == STATE_COLLAPSED) { 210 ViewCompat.offsetTopAndBottom(child, mMaxOffset); 211 } else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) { 212 ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); 213 } 214 if (mViewDragHelper == null) { 215 mViewDragHelper = ViewDragHelper.create(parent, mDragCallback); 216 } 217 mViewRef = new WeakReference<>(child); 218 mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); 219 return true; 220 } 221 222 @Override onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event)223 public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { 224 if (!child.isShown()) { 225 return false; 226 } 227 int action = MotionEventCompat.getActionMasked(event); 228 // Record the velocity 229 if (action == MotionEvent.ACTION_DOWN) { 230 reset(); 231 } 232 if (mVelocityTracker == null) { 233 mVelocityTracker = VelocityTracker.obtain(); 234 } 235 mVelocityTracker.addMovement(event); 236 switch (action) { 237 case MotionEvent.ACTION_UP: 238 case MotionEvent.ACTION_CANCEL: 239 mTouchingScrollingChild = false; 240 mActivePointerId = MotionEvent.INVALID_POINTER_ID; 241 // Reset the ignore flag 242 if (mIgnoreEvents) { 243 mIgnoreEvents = false; 244 return false; 245 } 246 break; 247 case MotionEvent.ACTION_DOWN: 248 int initialX = (int) event.getX(); 249 mInitialY = (int) event.getY(); 250 View scroll = mNestedScrollingChildRef.get(); 251 if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) { 252 mActivePointerId = event.getPointerId(event.getActionIndex()); 253 mTouchingScrollingChild = true; 254 } 255 mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID && 256 !parent.isPointInChildBounds(child, initialX, mInitialY); 257 break; 258 } 259 if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) { 260 return true; 261 } 262 // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because 263 // it is not the top most view of its parent. This is not necessary when the touch event is 264 // happening over the scrolling content as nested scrolling logic handles that case. 265 View scroll = mNestedScrollingChildRef.get(); 266 return action == MotionEvent.ACTION_MOVE && scroll != null && 267 !mIgnoreEvents && mState != STATE_DRAGGING && 268 !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) && 269 Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop(); 270 } 271 272 @Override onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event)273 public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { 274 if (!child.isShown()) { 275 return false; 276 } 277 int action = MotionEventCompat.getActionMasked(event); 278 if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) { 279 return true; 280 } 281 mViewDragHelper.processTouchEvent(event); 282 // Record the velocity 283 if (action == MotionEvent.ACTION_DOWN) { 284 reset(); 285 } 286 if (mVelocityTracker == null) { 287 mVelocityTracker = VelocityTracker.obtain(); 288 } 289 mVelocityTracker.addMovement(event); 290 // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it 291 // to capture the bottom sheet in case it is not captured and the touch slop is passed. 292 if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents) { 293 if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) { 294 mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex())); 295 } 296 } 297 return !mIgnoreEvents; 298 } 299 300 @Override onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes)301 public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, 302 View directTargetChild, View target, int nestedScrollAxes) { 303 mLastNestedScrollDy = 0; 304 mNestedScrolled = false; 305 return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; 306 } 307 308 @Override onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed)309 public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, 310 int dy, int[] consumed) { 311 View scrollingChild = mNestedScrollingChildRef.get(); 312 if (target != scrollingChild) { 313 return; 314 } 315 int currentTop = child.getTop(); 316 int newTop = currentTop - dy; 317 if (dy > 0) { // Upward 318 if (newTop < mMinOffset) { 319 consumed[1] = currentTop - mMinOffset; 320 ViewCompat.offsetTopAndBottom(child, -consumed[1]); 321 setStateInternal(STATE_EXPANDED); 322 } else { 323 consumed[1] = dy; 324 ViewCompat.offsetTopAndBottom(child, -dy); 325 setStateInternal(STATE_DRAGGING); 326 } 327 } else if (dy < 0) { // Downward 328 if (!ViewCompat.canScrollVertically(target, -1)) { 329 if (newTop <= mMaxOffset || mHideable) { 330 consumed[1] = dy; 331 ViewCompat.offsetTopAndBottom(child, -dy); 332 setStateInternal(STATE_DRAGGING); 333 } else { 334 consumed[1] = currentTop - mMaxOffset; 335 ViewCompat.offsetTopAndBottom(child, -consumed[1]); 336 setStateInternal(STATE_COLLAPSED); 337 } 338 } 339 } 340 dispatchOnSlide(child.getTop()); 341 mLastNestedScrollDy = dy; 342 mNestedScrolled = true; 343 } 344 345 @Override onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target)346 public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { 347 if (child.getTop() == mMinOffset) { 348 setStateInternal(STATE_EXPANDED); 349 return; 350 } 351 if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) { 352 return; 353 } 354 int top; 355 int targetState; 356 if (mLastNestedScrollDy > 0) { 357 top = mMinOffset; 358 targetState = STATE_EXPANDED; 359 } else if (mHideable && shouldHide(child, getYVelocity())) { 360 top = mParentHeight; 361 targetState = STATE_HIDDEN; 362 } else if (mLastNestedScrollDy == 0) { 363 int currentTop = child.getTop(); 364 if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { 365 top = mMinOffset; 366 targetState = STATE_EXPANDED; 367 } else { 368 top = mMaxOffset; 369 targetState = STATE_COLLAPSED; 370 } 371 } else { 372 top = mMaxOffset; 373 targetState = STATE_COLLAPSED; 374 } 375 if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) { 376 setStateInternal(STATE_SETTLING); 377 ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState)); 378 } else { 379 setStateInternal(targetState); 380 } 381 mNestedScrolled = false; 382 } 383 384 @Override onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY)385 public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, 386 float velocityX, float velocityY) { 387 return target == mNestedScrollingChildRef.get() && 388 (mState != STATE_EXPANDED || 389 super.onNestedPreFling(coordinatorLayout, child, target, 390 velocityX, velocityY)); 391 } 392 393 /** 394 * Sets the height of the bottom sheet when it is collapsed. 395 * 396 * @param peekHeight The height of the collapsed bottom sheet in pixels. 397 * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight 398 */ setPeekHeight(int peekHeight)399 public final void setPeekHeight(int peekHeight) { 400 mPeekHeight = Math.max(0, peekHeight); 401 mMaxOffset = mParentHeight - peekHeight; 402 } 403 404 /** 405 * Gets the height of the bottom sheet when it is collapsed. 406 * 407 * @return The height of the collapsed bottom sheet. 408 * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight 409 */ getPeekHeight()410 public final int getPeekHeight() { 411 return mPeekHeight; 412 } 413 414 /** 415 * Sets whether this bottom sheet can hide when it is swiped down. 416 * 417 * @param hideable {@code true} to make this bottom sheet hideable. 418 * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable 419 */ setHideable(boolean hideable)420 public void setHideable(boolean hideable) { 421 mHideable = hideable; 422 } 423 424 /** 425 * Gets whether this bottom sheet can hide when it is swiped down. 426 * 427 * @return {@code true} if this bottom sheet can hide. 428 * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable 429 */ isHideable()430 public boolean isHideable() { 431 return mHideable; 432 } 433 434 /** 435 * Sets whether this bottom sheet should skip the collapsed state when it is being hidden 436 * after it is expanded once. Setting this to true has no effect unless the sheet is hideable. 437 * 438 * @param skipCollapsed True if the bottom sheet should skip the collapsed state. 439 * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed 440 */ setSkipCollapsed(boolean skipCollapsed)441 public void setSkipCollapsed(boolean skipCollapsed) { 442 mSkipCollapsed = skipCollapsed; 443 } 444 445 /** 446 * Sets whether this bottom sheet should skip the collapsed state when it is being hidden 447 * after it is expanded once. 448 * 449 * @return Whether the bottom sheet should skip the collapsed state. 450 * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed 451 */ getSkipCollapsed()452 public boolean getSkipCollapsed() { 453 return mSkipCollapsed; 454 } 455 456 /** 457 * Sets a callback to be notified of bottom sheet events. 458 * 459 * @param callback The callback to notify when bottom sheet events occur. 460 */ setBottomSheetCallback(BottomSheetCallback callback)461 public void setBottomSheetCallback(BottomSheetCallback callback) { 462 mCallback = callback; 463 } 464 465 /** 466 * Sets the state of the bottom sheet. The bottom sheet will transition to that state with 467 * animation. 468 * 469 * @param state One of {@link #STATE_COLLAPSED}, {@link #STATE_EXPANDED}, or 470 * {@link #STATE_HIDDEN}. 471 */ setState(@tate int state)472 public final void setState(@State int state) { 473 if (state == mState) { 474 return; 475 } 476 if (mViewRef == null) { 477 // The view is not laid out yet; modify mState and let onLayoutChild handle it later 478 if (state == STATE_COLLAPSED || state == STATE_EXPANDED || 479 (mHideable && state == STATE_HIDDEN)) { 480 mState = state; 481 } 482 return; 483 } 484 V child = mViewRef.get(); 485 if (child == null) { 486 return; 487 } 488 int top; 489 if (state == STATE_COLLAPSED) { 490 top = mMaxOffset; 491 } else if (state == STATE_EXPANDED) { 492 top = mMinOffset; 493 } else if (mHideable && state == STATE_HIDDEN) { 494 top = mParentHeight; 495 } else { 496 throw new IllegalArgumentException("Illegal state argument: " + state); 497 } 498 setStateInternal(STATE_SETTLING); 499 if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) { 500 ViewCompat.postOnAnimation(child, new SettleRunnable(child, state)); 501 } 502 } 503 504 /** 505 * Gets the current state of the bottom sheet. 506 * 507 * @return One of {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link #STATE_DRAGGING}, 508 * and {@link #STATE_SETTLING}. 509 */ 510 @State getState()511 public final int getState() { 512 return mState; 513 } 514 setStateInternal(@tate int state)515 private void setStateInternal(@State int state) { 516 if (mState == state) { 517 return; 518 } 519 mState = state; 520 View bottomSheet = mViewRef.get(); 521 if (bottomSheet != null && mCallback != null) { 522 mCallback.onStateChanged(bottomSheet, state); 523 } 524 } 525 reset()526 private void reset() { 527 mActivePointerId = ViewDragHelper.INVALID_POINTER; 528 if (mVelocityTracker != null) { 529 mVelocityTracker.recycle(); 530 mVelocityTracker = null; 531 } 532 } 533 shouldHide(View child, float yvel)534 private boolean shouldHide(View child, float yvel) { 535 if (mSkipCollapsed) { 536 return true; 537 } 538 if (child.getTop() < mMaxOffset) { 539 // It should not hide, but collapse. 540 return false; 541 } 542 final float newTop = child.getTop() + yvel * HIDE_FRICTION; 543 return Math.abs(newTop - mMaxOffset) / (float) mPeekHeight > HIDE_THRESHOLD; 544 } 545 findScrollingChild(View view)546 private View findScrollingChild(View view) { 547 if (view instanceof NestedScrollingChild) { 548 return view; 549 } 550 if (view instanceof ViewGroup) { 551 ViewGroup group = (ViewGroup) view; 552 for (int i = 0, count = group.getChildCount(); i < count; i++) { 553 View scrollingChild = findScrollingChild(group.getChildAt(i)); 554 if (scrollingChild != null) { 555 return scrollingChild; 556 } 557 } 558 } 559 return null; 560 } 561 getYVelocity()562 private float getYVelocity() { 563 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 564 return VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId); 565 } 566 567 private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() { 568 569 @Override 570 public boolean tryCaptureView(View child, int pointerId) { 571 if (mState == STATE_DRAGGING) { 572 return false; 573 } 574 if (mTouchingScrollingChild) { 575 return false; 576 } 577 if (mState == STATE_EXPANDED && mActivePointerId == pointerId) { 578 View scroll = mNestedScrollingChildRef.get(); 579 if (scroll != null && ViewCompat.canScrollVertically(scroll, -1)) { 580 // Let the content scroll up 581 return false; 582 } 583 } 584 return mViewRef != null && mViewRef.get() == child; 585 } 586 587 @Override 588 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { 589 dispatchOnSlide(top); 590 } 591 592 @Override 593 public void onViewDragStateChanged(int state) { 594 if (state == ViewDragHelper.STATE_DRAGGING) { 595 setStateInternal(STATE_DRAGGING); 596 } 597 } 598 599 @Override 600 public void onViewReleased(View releasedChild, float xvel, float yvel) { 601 int top; 602 @State int targetState; 603 if (yvel < 0) { // Moving up 604 top = mMinOffset; 605 targetState = STATE_EXPANDED; 606 } else if (mHideable && shouldHide(releasedChild, yvel)) { 607 top = mParentHeight; 608 targetState = STATE_HIDDEN; 609 } else if (yvel == 0.f) { 610 int currentTop = releasedChild.getTop(); 611 if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { 612 top = mMinOffset; 613 targetState = STATE_EXPANDED; 614 } else { 615 top = mMaxOffset; 616 targetState = STATE_COLLAPSED; 617 } 618 } else { 619 top = mMaxOffset; 620 targetState = STATE_COLLAPSED; 621 } 622 if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) { 623 setStateInternal(STATE_SETTLING); 624 ViewCompat.postOnAnimation(releasedChild, 625 new SettleRunnable(releasedChild, targetState)); 626 } else { 627 setStateInternal(targetState); 628 } 629 } 630 631 @Override 632 public int clampViewPositionVertical(View child, int top, int dy) { 633 return MathUtils.constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset); 634 } 635 636 @Override 637 public int clampViewPositionHorizontal(View child, int left, int dx) { 638 return child.getLeft(); 639 } 640 641 @Override 642 public int getViewVerticalDragRange(View child) { 643 if (mHideable) { 644 return mParentHeight - mMinOffset; 645 } else { 646 return mMaxOffset - mMinOffset; 647 } 648 } 649 }; 650 dispatchOnSlide(int top)651 private void dispatchOnSlide(int top) { 652 View bottomSheet = mViewRef.get(); 653 if (bottomSheet != null && mCallback != null) { 654 if (top > mMaxOffset) { 655 mCallback.onSlide(bottomSheet, (float) (mMaxOffset - top) / mPeekHeight); 656 } else { 657 mCallback.onSlide(bottomSheet, 658 (float) (mMaxOffset - top) / ((mMaxOffset - mMinOffset))); 659 } 660 } 661 } 662 663 private class SettleRunnable implements Runnable { 664 665 private final View mView; 666 667 @State 668 private final int mTargetState; 669 SettleRunnable(View view, @State int targetState)670 SettleRunnable(View view, @State int targetState) { 671 mView = view; 672 mTargetState = targetState; 673 } 674 675 @Override run()676 public void run() { 677 if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) { 678 ViewCompat.postOnAnimation(mView, this); 679 } else { 680 setStateInternal(mTargetState); 681 } 682 } 683 } 684 685 protected static class SavedState extends AbsSavedState { 686 @State 687 final int state; 688 SavedState(Parcel source)689 public SavedState(Parcel source) { 690 this(source, null); 691 } 692 SavedState(Parcel source, ClassLoader loader)693 public SavedState(Parcel source, ClassLoader loader) { 694 super(source, loader); 695 //noinspection ResourceType 696 state = source.readInt(); 697 } 698 SavedState(Parcelable superState, @State int state)699 public SavedState(Parcelable superState, @State int state) { 700 super(superState); 701 this.state = state; 702 } 703 704 @Override writeToParcel(Parcel out, int flags)705 public void writeToParcel(Parcel out, int flags) { 706 super.writeToParcel(out, flags); 707 out.writeInt(state); 708 } 709 710 public static final Creator<SavedState> CREATOR = ParcelableCompat.newCreator( 711 new ParcelableCompatCreatorCallbacks<SavedState>() { 712 @Override 713 public SavedState createFromParcel(Parcel in, ClassLoader loader) { 714 return new SavedState(in, loader); 715 } 716 717 @Override 718 public SavedState[] newArray(int size) { 719 return new SavedState[size]; 720 } 721 }); 722 } 723 724 /** 725 * A utility function to get the {@link BottomSheetBehavior} associated with the {@code view}. 726 * 727 * @param view The {@link View} with {@link BottomSheetBehavior}. 728 * @return The {@link BottomSheetBehavior} associated with the {@code view}. 729 */ 730 @SuppressWarnings("unchecked") from(V view)731 public static <V extends View> BottomSheetBehavior<V> from(V view) { 732 ViewGroup.LayoutParams params = view.getLayoutParams(); 733 if (!(params instanceof CoordinatorLayout.LayoutParams)) { 734 throw new IllegalArgumentException("The view is not a child of CoordinatorLayout"); 735 } 736 CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params) 737 .getBehavior(); 738 if (!(behavior instanceof BottomSheetBehavior)) { 739 throw new IllegalArgumentException( 740 "The view is not associated with BottomSheetBehavior"); 741 } 742 return (BottomSheetBehavior<V>) behavior; 743 } 744 745 } 746