Merge "BottomSheet supports nested scrolling" into mnc-ub-dev
diff --git a/design/src/android/support/design/widget/BottomSheetBehavior.java b/design/src/android/support/design/widget/BottomSheetBehavior.java
index 1be223d..fbd725d 100644
--- a/design/src/android/support/design/widget/BottomSheetBehavior.java
+++ b/design/src/android/support/design/widget/BottomSheetBehavior.java
@@ -18,7 +18,6 @@
 
 import android.content.Context;
 import android.content.res.TypedArray;
-import android.os.Build;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.support.annotation.IntDef;
@@ -26,13 +25,15 @@
 import android.support.design.R;
 import android.support.v4.view.MotionEventCompat;
 import android.support.v4.view.NestedScrollingChild;
+import android.support.v4.view.VelocityTrackerCompat;
 import android.support.v4.view.ViewCompat;
 import android.support.v4.widget.ViewDragHelper;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
+import android.view.VelocityTracker;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.ViewGroup;
-import android.view.ViewParent;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -104,8 +105,7 @@
 
     private static final float HIDE_FRICTION = 0.1f;
 
-    // Whether to enable workaround for black non-rendered square
-    private static final boolean NEEDS_INVALIDATING = Build.VERSION.SDK_INT < 23;
+    private float mMaximumVelocity;
 
     private int mPeekHeight;
 
@@ -122,6 +122,8 @@
 
     private boolean mIgnoreEvents;
 
+    private int mLastNestedScrollDy;
+
     private int mParentHeight;
 
     private WeakReference<V> mViewRef;
@@ -130,7 +132,9 @@
 
     private BottomSheetCallback mCallback;
 
-    private int mScrollingChildPointerId = MotionEvent.INVALID_POINTER_ID;
+    private VelocityTracker mVelocityTracker;
+
+    private int mActivePointerId;
 
     /**
      * Default constructor for instantiating BottomSheetBehaviors.
@@ -152,6 +156,8 @@
                 R.styleable.BottomSheetBehavior_Params_behavior_peekHeight, 0));
         setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Params_behavior_hideable, false));
         a.recycle();
+        ViewConfiguration configuration = ViewConfiguration.get(context);
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
     }
 
     @Override
@@ -202,10 +208,18 @@
             return false;
         }
         int action = MotionEventCompat.getActionMasked(event);
+        // Record the velocity
+        if (action == MotionEvent.ACTION_DOWN) {
+            reset();
+        }
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
         switch (action) {
             case MotionEvent.ACTION_UP:
             case MotionEvent.ACTION_CANCEL:
-                mScrollingChildPointerId = MotionEvent.INVALID_POINTER_ID;
+                mActivePointerId = MotionEvent.INVALID_POINTER_ID;
                 // Reset the ignore flag
                 if (mIgnoreEvents) {
                     mIgnoreEvents = false;
@@ -217,11 +231,9 @@
                 int y = (int) event.getY();
                 View scroll = mNestedScrollingChildRef.get();
                 if (scroll != null && parent.isPointInChildBounds(scroll, x, y)) {
-                    mScrollingChildPointerId = event.getPointerId(event.getActionIndex());
-                } else {
-                    mScrollingChildPointerId = MotionEvent.INVALID_POINTER_ID;
+                    mActivePointerId = event.getPointerId(event.getActionIndex());
                 }
-                mIgnoreEvents = mScrollingChildPointerId == MotionEvent.INVALID_POINTER_ID &&
+                mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
                         !parent.isPointInChildBounds(child, x, y);
                 break;
         }
@@ -233,10 +245,100 @@
         if (!child.isShown()) {
             return false;
         }
+        int action = MotionEventCompat.getActionMasked(event);
+        if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
+            return true;
+        }
         mViewDragHelper.processTouchEvent(event);
+        // Record the velocity
+        if (action == MotionEvent.ACTION_DOWN) {
+            reset();
+        }
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
         return true;
     }
 
+    @Override
+    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
+            View directTargetChild, View target, int nestedScrollAxes) {
+        mLastNestedScrollDy = 0;
+        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
+    }
+
+    @Override
+    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
+            int dy, int[] consumed) {
+        View scrollingChild = mNestedScrollingChildRef.get();
+        if (target != scrollingChild) {
+            return;
+        }
+        int currentTop = child.getTop();
+        int newTop = currentTop - dy;
+        if (dy > 0) { // Upward
+            if (newTop < mMinOffset) {
+                consumed[1] = currentTop - mMinOffset;
+                ViewCompat.offsetTopAndBottom(child, -consumed[1]);
+                setStateInternal(STATE_EXPANDED);
+            } else {
+                consumed[1] = dy;
+                ViewCompat.offsetTopAndBottom(child, -dy);
+                setStateInternal(STATE_DRAGGING);
+            }
+        } else if (dy < 0) { // Downward
+            if (!ViewCompat.canScrollVertically(target, -1)) {
+                if (newTop <= mMaxOffset || mHideable) {
+                    consumed[1] = dy;
+                    ViewCompat.offsetTopAndBottom(child, -dy);
+                    setStateInternal(STATE_DRAGGING);
+                } else {
+                    consumed[1] = currentTop - mMaxOffset;
+                    ViewCompat.offsetTopAndBottom(child, -consumed[1]);
+                    setStateInternal(STATE_COLLAPSED);
+                }
+            }
+        }
+        dispatchOnSlide(child.getTop());
+        mLastNestedScrollDy = dy;
+    }
+
+    @Override
+    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
+        if (child.getTop() == mMinOffset || target != mNestedScrollingChildRef.get() ||
+                mLastNestedScrollDy == 0) {
+            return;
+        }
+        int top;
+        int targetState;
+        if (mLastNestedScrollDy > 0) {
+            top = mMinOffset;
+            targetState = STATE_EXPANDED;
+        } else if (mHideable && shouldHide(child, getYVelocity())) {
+            top = mParentHeight;
+            targetState = STATE_HIDDEN;
+        } else {
+            top = mMaxOffset;
+            targetState = STATE_COLLAPSED;
+        }
+        if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
+            setStateInternal(STATE_SETTLING);
+            ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
+        } else {
+            setStateInternal(targetState);
+        }
+    }
+
+    @Override
+    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
+            float velocityX, float velocityY) {
+        return target == mNestedScrollingChildRef.get() &&
+                (mState != STATE_EXPANDED ||
+                        super.onNestedPreFling(coordinatorLayout, child, target,
+                                velocityX, velocityY));
+    }
+
     /**
      * Sets the height of the bottom sheet when it is collapsed.
      *
@@ -337,6 +439,14 @@
         }
     }
 
+    private void reset() {
+        mActivePointerId = ViewDragHelper.INVALID_POINTER;
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
     private boolean shouldHide(View child, float yvel) {
         if (child.getTop() < mMaxOffset) {
             // It should not hide, but collapse.
@@ -362,11 +472,19 @@
         return null;
     }
 
+    private float getYVelocity() {
+        mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+        return VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId);
+    }
+
     private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
 
         @Override
         public boolean tryCaptureView(View child, int pointerId) {
-            if (mState == STATE_EXPANDED && mScrollingChildPointerId == pointerId) {
+            if (mState == STATE_DRAGGING) {
+                return false;
+            }
+            if (mState == STATE_EXPANDED && mActivePointerId == pointerId) {
                 View scroll = mNestedScrollingChildRef.get();
                 if (scroll != null && ViewCompat.canScrollVertically(scroll, -1)) {
                     // Let the content scroll up
@@ -379,18 +497,6 @@
         @Override
         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
             dispatchOnSlide(top);
-            if (NEEDS_INVALIDATING) {
-                if (dy < 0) { // Upward
-                    changedView.invalidate();
-                } else { // Downward
-                    ViewParent parent = changedView.getParent();
-                    if (parent instanceof View) {
-                        View v = (View) parent;
-                        v.invalidate(changedView.getLeft(), top - dy,
-                                changedView.getRight(), v.getHeight());
-                    }
-                }
-            }
         }
 
         @Override
@@ -465,13 +571,6 @@
         SettleRunnable(View view, @State int targetState) {
             mView = view;
             mTargetState = targetState;
-            if (NEEDS_INVALIDATING) {
-                // We need to invalidate the parent here, or the following animation won't be drawn.
-                ViewParent parent = mView.getParent();
-                if (parent instanceof View) {
-                    ((View) parent).invalidate();
-                }
-            }
         }
 
         @Override