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