Initial implementation of BottomSheet

This adds persistent bottom sheets as a CoordinatorLayout.Behavior.

Bug: 23989269
Change-Id: Iaff739523ba68ee2b9d8214264a6118e0f8c1ad1
diff --git a/design/api/current.txt b/design/api/current.txt
index f68ab1b..167faa6 100644
--- a/design/api/current.txt
+++ b/design/api/current.txt
@@ -70,6 +70,26 @@
     method public void setOverlayTop(int);
   }
 
+  public class BottomSheetBehavior extends android.support.design.widget.CoordinatorLayout.Behavior {
+    ctor public BottomSheetBehavior();
+    ctor public BottomSheetBehavior(android.content.Context, android.util.AttributeSet);
+    method public static android.support.design.widget.BottomSheetBehavior<V> from(V);
+    method public final int getPeekHeight();
+    method public final int getState();
+    method public final void setPeekHeight(int);
+    method public final void setState(int);
+    field public static final int STATE_COLLAPSED = 4; // 0x4
+    field public static final int STATE_DRAGGING = 1; // 0x1
+    field public static final int STATE_EXPANDED = 3; // 0x3
+    field public static final int STATE_SETTLING = 2; // 0x2
+  }
+
+  protected static class BottomSheetBehavior.SavedState extends android.view.View.BaseSavedState {
+    ctor public BottomSheetBehavior.SavedState(android.os.Parcel);
+    ctor public BottomSheetBehavior.SavedState(android.os.Parcelable, int);
+    field public static final android.os.Parcelable.Creator<android.support.design.widget.BottomSheetBehavior.SavedState> CREATOR;
+  }
+
   public class CollapsingToolbarLayout extends android.widget.FrameLayout {
     ctor public CollapsingToolbarLayout(android.content.Context);
     ctor public CollapsingToolbarLayout(android.content.Context, android.util.AttributeSet);
diff --git a/design/res-public/values/public_attrs.xml b/design/res-public/values/public_attrs.xml
index 8b9e51c..8c8eb0d 100644
--- a/design/res-public/values/public_attrs.xml
+++ b/design/res-public/values/public_attrs.xml
@@ -70,4 +70,5 @@
     <public type="attr" name="title"/>
     <public type="attr" name="titleEnabled"/>
     <public type="attr" name="toolbarId"/>
+    <public type="attr" name="behavior_peekHeight"/>
 </resources>
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index 81121fd..dd8e64f 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -334,5 +334,10 @@
         <attr name="layout_collapseParallaxMultiplier" format="float"/>
     </declare-styleable>
 
+    <declare-styleable name="BottomSheetBehavior_Params">
+        <!-- The height of the bottom sheet when it is collapsed. -->
+        <attr name="behavior_peekHeight" format="dimension"/>
+    </declare-styleable>
+
 </resources>
 
diff --git a/design/src/android/support/design/widget/BottomSheetBehavior.java b/design/src/android/support/design/widget/BottomSheetBehavior.java
new file mode 100644
index 0000000..cd24414
--- /dev/null
+++ b/design/src/android/support/design/widget/BottomSheetBehavior.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.IntDef;
+import android.support.design.R;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewDragHelper;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+
+
+/**
+ * An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as
+ * a bottom sheet.
+ */
+public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
+
+    /**
+     * The bottom sheet is dragging.
+     */
+    public static final int STATE_DRAGGING = 1;
+
+    /**
+     * The bottom sheet is settling.
+     */
+    public static final int STATE_SETTLING = 2;
+
+    /**
+     * The bottom sheet is expanded.
+     */
+    public static final int STATE_EXPANDED = 3;
+
+    /**
+     * The bottom sheet is collapsed.
+     */
+    public static final int STATE_COLLAPSED = 4;
+
+    /** @hide */
+    @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface State {}
+
+    private int mPeekHeight;
+
+    private int mMinOffset;
+
+    private int mMaxOffset;
+
+    @State
+    private int mState = STATE_COLLAPSED;
+
+    private ViewDragHelper mViewDragHelper;
+
+    private boolean mIgnoreEvents;
+
+    private int mLastNestedScrollDy;
+
+    private int mParentHeight;
+
+    private WeakReference<V> mViewRef;
+
+    /**
+     * Default constructor for instantiating BottomSheetBehaviors.
+     */
+    public BottomSheetBehavior() {
+    }
+
+    /**
+     * Default constructor for inflating BottomSheetBehaviors from layout.
+     *
+     * @param context The {@link Context}.
+     * @param attrs   The {@link AttributeSet}.
+     */
+    public BottomSheetBehavior(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.BottomSheetBehavior_Params);
+        setPeekHeight(a.getDimensionPixelSize(
+                R.styleable.BottomSheetBehavior_Params_behavior_peekHeight, 0));
+        a.recycle();
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
+        return new SavedState(super.onSaveInstanceState(parent, child), mState);
+    }
+
+    @Override
+    public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) {
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(parent, child, ss.getSuperState());
+        mState = ss.state;
+        // Intermediate states are restored as collapsed state
+        if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
+            mState = STATE_COLLAPSED;
+        }
+    }
+
+    @Override
+    public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
+        // First let the parent lay it out
+        parent.onLayoutChild(child, layoutDirection);
+        // Offset the bottom sheet
+        mParentHeight = parent.getHeight();
+        mMinOffset = Math.max(0, mParentHeight - child.getHeight());
+        mMaxOffset = mParentHeight - mPeekHeight;
+        if (mState == STATE_EXPANDED) {
+            ViewCompat.offsetTopAndBottom(child, mMinOffset);
+        } else {
+            ViewCompat.offsetTopAndBottom(child, mMaxOffset);
+            mState = STATE_COLLAPSED;
+        }
+        if (mViewDragHelper == null) {
+            mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
+        }
+        mViewRef = new WeakReference<>(child);
+        return true;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
+        int action = MotionEventCompat.getActionMasked(event);
+        switch (action) {
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                // Reset the ignore flag
+                if (mIgnoreEvents) {
+                    mIgnoreEvents = false;
+                    return false;
+                }
+                break;
+            case MotionEvent.ACTION_DOWN:
+                mIgnoreEvents = !parent.isPointInChildBounds(child,
+                        (int) event.getX(), (int) event.getY());
+                break;
+        }
+        return !mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
+        mViewDragHelper.processTouchEvent(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) {
+        int currentTop = child.getTop();
+        int newTop = currentTop - dy;
+        if (dy > 0) { // Scrolling up
+            if (newTop < mMinOffset) {
+                consumed[1] = currentTop - mMinOffset;
+                child.offsetTopAndBottom(-consumed[1]);
+                setStateInternal(STATE_EXPANDED);
+            } else {
+                consumed[1] = dy;
+                child.offsetTopAndBottom(-dy);
+                setStateInternal(STATE_DRAGGING);
+            }
+        } else if (dy < 0) { // Scrolling down
+            if (!ViewCompat.canScrollVertically(target, -1)) {
+                if (newTop > mMaxOffset) {
+                    consumed[1] = currentTop - mMaxOffset;
+                    child.offsetTopAndBottom(-consumed[1]);
+                    setStateInternal(STATE_COLLAPSED);
+                } else {
+                    consumed[1] = dy;
+                    child.offsetTopAndBottom(-dy);
+                    setStateInternal(STATE_DRAGGING);
+                }
+            }
+        }
+        mLastNestedScrollDy = dy;
+    }
+
+    @Override
+    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
+        if (mLastNestedScrollDy == 0 || child.getTop() == mMinOffset) {
+            return;
+        }
+        int top;
+        int targetState;
+        if (mLastNestedScrollDy > 0) {
+            top = mMinOffset;
+            targetState = STATE_EXPANDED;
+        } else {
+            top = mMaxOffset;
+            targetState = STATE_COLLAPSED;
+        }
+        setStateInternal(STATE_SETTLING);
+        if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
+            ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
+        }
+    }
+
+    /**
+     * Sets the height of the bottom sheet when it is collapsed.
+     *
+     * @param peekHeight The height of the collapsed bottom sheet in pixels.
+     * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Params_behavior_peekHeight
+     */
+    public final void setPeekHeight(int peekHeight) {
+        mPeekHeight = Math.max(0, peekHeight);
+        mMaxOffset = mParentHeight - peekHeight;
+    }
+
+    /**
+     * Gets the height of the bottom sheet when it is collapsed.
+     *
+     * @return The height of the collapsed bottom sheet.
+     * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Params_behavior_peekHeight
+     */
+    public final int getPeekHeight() {
+        return mPeekHeight;
+    }
+
+    /**
+     * Sets the state of the bottom sheet. The bottom sheet will transition to that state with
+     * animation.
+     *
+     * @param state Either {@link #STATE_COLLAPSED} or {@link #STATE_EXPANDED}.
+     */
+    public final void setState(@State int state) {
+        V child = mViewRef.get();
+        if (child == null) {
+            return;
+        }
+        int top;
+        if (state == STATE_COLLAPSED) {
+            top = mMaxOffset;
+        } else if (state == STATE_EXPANDED) {
+            top = mMinOffset;
+        } else {
+            throw new IllegalArgumentException("Illegal state argument: " + state);
+        }
+        setStateInternal(STATE_SETTLING);
+        if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
+            ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
+        }
+    }
+
+    /**
+     * Gets the current state of the bottom sheet.
+     *
+     * @return One of {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link #STATE_DRAGGING},
+     * and {@link #STATE_SETTLING}.
+     */
+    @State
+    public final int getState() {
+        return mState;
+    }
+
+    private void setStateInternal(@State int state) {
+        if (mState == state) {
+            return;
+        }
+        mState = state;
+        // TODO: Invoke listeners.
+    }
+
+    private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
+
+        @Override
+        public boolean tryCaptureView(View child, int pointerId) {
+            return true;
+        }
+
+        @Override
+        public void onViewDragStateChanged(int state) {
+            if (state == ViewDragHelper.STATE_DRAGGING) {
+                setStateInternal(STATE_DRAGGING);
+            }
+        }
+
+        @Override
+        public void onViewReleased(View releasedChild, float xvel, float yvel) {
+            int top;
+            @State int targetState;
+            if (yvel < 0) {
+                top = mMinOffset;
+                targetState = STATE_EXPANDED;
+            } else {
+                top = mMaxOffset;
+                targetState = STATE_COLLAPSED;
+            }
+            setStateInternal(STATE_SETTLING);
+            if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
+                ViewCompat.postOnAnimation(releasedChild,
+                        new SettleRunnable(releasedChild, targetState));
+            }
+        }
+
+        @Override
+        public int clampViewPositionVertical(View child, int top, int dy) {
+            return MathUtils.constrain(top, mMinOffset, mMaxOffset);
+        }
+
+        @Override
+        public int clampViewPositionHorizontal(View child, int left, int dx) {
+            return child.getLeft();
+        }
+    };
+
+    private class SettleRunnable implements Runnable {
+
+        private final View mView;
+
+        @State
+        private final int mTargetState;
+
+        SettleRunnable(View view, @State int targetState) {
+            mView = view;
+            mTargetState = targetState;
+        }
+
+        @Override
+        public void run() {
+            if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
+                ViewCompat.postOnAnimation(mView, this);
+            } else {
+                setStateInternal(mTargetState);
+            }
+        }
+    }
+
+    protected static class SavedState extends View.BaseSavedState {
+
+        @State
+        final int state;
+
+        public SavedState(Parcel source) {
+            super(source);
+            //noinspection ResourceType
+            state = source.readInt();
+        }
+
+        public SavedState(Parcelable superState, @State int state) {
+            super(superState);
+            this.state = state;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(state);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+                    @Override
+                    public SavedState createFromParcel(Parcel source) {
+                        return new SavedState(source);
+                    }
+
+                    @Override
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
+    }
+
+    /*
+     * A utility function to get the {@link BottomSheetBehavior} associated with the {@code view}.
+     *
+     * @param view The {@link View} with {@link BottomSheetBehavior}.
+     * @return The {@link BottomSheetBehavior} associated with the {@code view}.
+     */
+    @SuppressWarnings("unchecked")
+    public static <V extends View> BottomSheetBehavior<V> from(V view) {
+        ViewGroup.LayoutParams params = view.getLayoutParams();
+        if (!(params instanceof CoordinatorLayout.LayoutParams)) {
+            throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
+        }
+        CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
+                .getBehavior();
+        if (!(behavior instanceof BottomSheetBehavior)) {
+            throw new IllegalArgumentException(
+                    "The view is not associated with BottomSheetBehavior");
+        }
+        return (BottomSheetBehavior<V>) behavior;
+    }
+
+}