| /* |
| * 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) { |
| if (mViewRef == null) { |
| return; |
| } |
| 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; |
| } |
| |
| } |