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