[go: nahoru, domu]

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 android.content.Context;
20import android.content.res.TypedArray;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.support.annotation.IntDef;
24import android.support.annotation.NonNull;
25import android.support.design.R;
26import android.support.v4.os.ParcelableCompat;
27import android.support.v4.os.ParcelableCompatCreatorCallbacks;
28import android.support.v4.view.AbsSavedState;
29import android.support.v4.view.MotionEventCompat;
30import android.support.v4.view.NestedScrollingChild;
31import android.support.v4.view.VelocityTrackerCompat;
32import android.support.v4.view.ViewCompat;
33import android.support.v4.widget.ViewDragHelper;
34import android.util.AttributeSet;
35import android.view.MotionEvent;
36import android.view.VelocityTracker;
37import android.view.View;
38import android.view.ViewConfiguration;
39import android.view.ViewGroup;
40
41import java.lang.annotation.Retention;
42import java.lang.annotation.RetentionPolicy;
43import 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 */
50public 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         */
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         */
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     */
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     */
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
177    public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
178        return new SavedState(super.onSaveInstanceState(parent, child), mState);
179    }
180
181    @Override
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
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
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
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
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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
511    public final int getState() {
512        return mState;
513    }
514
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
526    private void reset() {
527        mActivePointerId = ViewDragHelper.INVALID_POINTER;
528        if (mVelocityTracker != null) {
529            mVelocityTracker.recycle();
530            mVelocityTracker = null;
531        }
532    }
533
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
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
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
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
670        SettleRunnable(View view, @State int targetState) {
671            mView = view;
672            mTargetState = targetState;
673        }
674
675        @Override
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
689        public SavedState(Parcel source) {
690            this(source, null);
691        }
692
693        public SavedState(Parcel source, ClassLoader loader) {
694            super(source, loader);
695            //noinspection ResourceType
696            state = source.readInt();
697        }
698
699        public SavedState(Parcelable superState, @State int state) {
700            super(superState);
701            this.state = state;
702        }
703
704        @Override
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")
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