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