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