Merge "Initial implementation of BottomSheet" into mnc-ub-dev
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;
+ }
+
+}