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