From 044982ac2b65aabbf7797d898cc4e257f01839c2 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 4 Jan 2018 16:57:01 +0200 Subject: [PATCH 1/3] Copy BottomSheetBehavior from support library --- .../tools/ExtendedBottomSheetBehavior.java | 831 ++++++++++++++++++ 1 file changed, 831 insertions(+) create mode 100644 OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java diff --git a/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java b/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java new file mode 100644 index 0000000000..0dffcc3c9c --- /dev/null +++ b/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java @@ -0,0 +1,831 @@ +/* + * 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 net.osmand.plus.widgets.tools; + +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.annotation.NonNull; +import android.support.annotation.RestrictTo; +import android.support.annotation.VisibleForTesting; +import android.support.design.R; +import android.support.design.widget.CoordinatorLayout; +import android.support.v4.math.MathUtils; +import android.support.v4.view.AbsSavedState; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.ViewDragHelper; +import android.util.AttributeSet; +import android.util.TypedValue; +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; +import java.lang.ref.WeakReference; + +import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; + + +/** + * An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as + * a bottom sheet. + */ +public class ExtendedBottomSheetBehavior extends CoordinatorLayout.Behavior { + + /** + * Callback for monitoring events about bottom sheets. + */ + public abstract static class BottomSheetCallback { + + /** + * Called when the bottom sheet changes its state. + * + * @param bottomSheet The bottom sheet view. + * @param newState The new state. This will be one of {@link #STATE_DRAGGING}, + * {@link #STATE_SETTLING}, {@link #STATE_EXPANDED}, + * {@link #STATE_COLLAPSED}, or {@link #STATE_HIDDEN}. + */ + public abstract void onStateChanged(@NonNull View bottomSheet, @State int newState); + + /** + * Called when the bottom sheet is being dragged. + * + * @param bottomSheet The bottom sheet view. + * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset + * increases as this bottom sheet is moving upward. From 0 to 1 the sheet + * is between collapsed and expanded states and from -1 to 0 it is + * between hidden and collapsed states. + */ + public abstract void onSlide(@NonNull View bottomSheet, float slideOffset); + } + + /** + * 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; + + /** + * The bottom sheet is hidden. + */ + public static final int STATE_HIDDEN = 5; + + /** + * @hide + */ + @RestrictTo(LIBRARY_GROUP) + @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN}) + @Retention(RetentionPolicy.SOURCE) + public @interface State { + } + + /** + * Peek at the 16:9 ratio keyline of its parent. + *

+ *

This can be used as a parameter for {@link #setPeekHeight(int)}. + * {@link #getPeekHeight()} will return this when the value is set.

+ */ + public static final int PEEK_HEIGHT_AUTO = -1; + + private static final float HIDE_THRESHOLD = 0.5f; + + private static final float HIDE_FRICTION = 0.1f; + + private float mMaximumVelocity; + + private int mPeekHeight; + + private boolean mPeekHeightAuto; + + private int mPeekHeightMin; + + int mMinOffset; + + int mMaxOffset; + + boolean mHideable; + + private boolean mSkipCollapsed; + + @State + int mState = STATE_COLLAPSED; + + ViewDragHelper mViewDragHelper; + + private boolean mIgnoreEvents; + + private int mLastNestedScrollDy; + + private boolean mNestedScrolled; + + int mParentHeight; + + WeakReference mViewRef; + + WeakReference mNestedScrollingChildRef; + + private BottomSheetCallback mCallback; + + private VelocityTracker mVelocityTracker; + + int mActivePointerId; + + private int mInitialY; + + boolean mTouchingScrollingChild; + + /** + * Default constructor for instantiating BottomSheetBehaviors. + */ + public ExtendedBottomSheetBehavior() { + } + + /** + * Default constructor for inflating BottomSheetBehaviors from layout. + * + * @param context The {@link Context}. + * @param attrs The {@link AttributeSet}. + */ + public ExtendedBottomSheetBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.BottomSheetBehavior_Layout); + TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight); + if (value != null && value.data == PEEK_HEIGHT_AUTO) { + setPeekHeight(value.data); + } else { + setPeekHeight(a.getDimensionPixelSize( + R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO)); + } + setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false)); + setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed, + false)); + a.recycle(); + ViewConfiguration configuration = ViewConfiguration.get(context); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @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()); + // Intermediate states are restored as collapsed state + if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) { + mState = STATE_COLLAPSED; + } else { + mState = ss.state; + } + } + + @Override + public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { + if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) { + ViewCompat.setFitsSystemWindows(child, true); + } + int savedTop = child.getTop(); + // First let the parent lay it out + parent.onLayoutChild(child, layoutDirection); + // Offset the bottom sheet + mParentHeight = parent.getHeight(); + int peekHeight; + if (mPeekHeightAuto) { + if (mPeekHeightMin == 0) { + mPeekHeightMin = parent.getResources().getDimensionPixelSize( + R.dimen.design_bottom_sheet_peek_height_min); + } + peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16); + } else { + peekHeight = mPeekHeight; + } + mMinOffset = Math.max(0, mParentHeight - child.getHeight()); + mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset); + if (mState == STATE_EXPANDED) { + ViewCompat.offsetTopAndBottom(child, mMinOffset); + } else if (mHideable && mState == STATE_HIDDEN) { + ViewCompat.offsetTopAndBottom(child, mParentHeight); + } else if (mState == STATE_COLLAPSED) { + ViewCompat.offsetTopAndBottom(child, mMaxOffset); + } else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) { + ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop()); + } + if (mViewDragHelper == null) { + mViewDragHelper = ViewDragHelper.create(parent, mDragCallback); + } + mViewRef = new WeakReference<>(child); + mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child)); + return true; + } + + @Override + public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + if (!child.isShown()) { + mIgnoreEvents = true; + return false; + } + int action = event.getActionMasked(); + // 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: + mTouchingScrollingChild = false; + mActivePointerId = MotionEvent.INVALID_POINTER_ID; + // Reset the ignore flag + if (mIgnoreEvents) { + mIgnoreEvents = false; + return false; + } + break; + case MotionEvent.ACTION_DOWN: + int initialX = (int) event.getX(); + mInitialY = (int) event.getY(); + View scroll = mNestedScrollingChildRef != null + ? mNestedScrollingChildRef.get() : null; + if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) { + mActivePointerId = event.getPointerId(event.getActionIndex()); + mTouchingScrollingChild = true; + } + mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID && + !parent.isPointInChildBounds(child, initialX, mInitialY); + break; + } + if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) { + return true; + } + // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because + // it is not the top most view of its parent. This is not necessary when the touch event is + // happening over the scrolling content as nested scrolling logic handles that case. + View scroll = mNestedScrollingChildRef.get(); + return action == MotionEvent.ACTION_MOVE && scroll != null && + !mIgnoreEvents && mState != STATE_DRAGGING && + !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) && + Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop(); + } + + @Override + public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) { + if (!child.isShown()) { + return false; + } + int action = event.getActionMasked(); + 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); + // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it + // to capture the bottom sheet in case it is not captured and the touch slop is passed. + if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents) { + if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) { + mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex())); + } + } + return !mIgnoreEvents; + } + + @Override + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, + View directTargetChild, View target, int nestedScrollAxes) { + mLastNestedScrollDy = 0; + mNestedScrolled = false; + 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 (!target.canScrollVertically(-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; + mNestedScrolled = true; + } + + @Override + public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { + if (child.getTop() == mMinOffset) { + setStateInternal(STATE_EXPANDED); + return; + } + if (mNestedScrollingChildRef == null || target != mNestedScrollingChildRef.get() + || !mNestedScrolled) { + 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 if (mLastNestedScrollDy == 0) { + int currentTop = child.getTop(); + if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { + top = mMinOffset; + targetState = STATE_EXPANDED; + } else { + top = mMaxOffset; + targetState = STATE_COLLAPSED; + } + } 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); + } + mNestedScrolled = false; + } + + @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. + * + * @param peekHeight The height of the collapsed bottom sheet in pixels, or + * {@link #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically + * at 16:9 ratio keyline. + * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + */ + public final void setPeekHeight(int peekHeight) { + boolean layout = false; + if (peekHeight == PEEK_HEIGHT_AUTO) { + if (!mPeekHeightAuto) { + mPeekHeightAuto = true; + layout = true; + } + } else if (mPeekHeightAuto || mPeekHeight != peekHeight) { + mPeekHeightAuto = false; + mPeekHeight = Math.max(0, peekHeight); + mMaxOffset = mParentHeight - peekHeight; + layout = true; + } + if (layout && mState == STATE_COLLAPSED && mViewRef != null) { + V view = mViewRef.get(); + if (view != null) { + view.requestLayout(); + } + } + } + + /** + * Gets the height of the bottom sheet when it is collapsed. + * + * @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO} + * if the sheet is configured to peek automatically at 16:9 ratio keyline + * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight + */ + public final int getPeekHeight() { + return mPeekHeightAuto ? PEEK_HEIGHT_AUTO : mPeekHeight; + } + + /** + * Sets whether this bottom sheet can hide when it is swiped down. + * + * @param hideable {@code true} to make this bottom sheet hideable. + * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable + */ + public void setHideable(boolean hideable) { + mHideable = hideable; + } + + /** + * Gets whether this bottom sheet can hide when it is swiped down. + * + * @return {@code true} if this bottom sheet can hide. + * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable + */ + public boolean isHideable() { + return mHideable; + } + + /** + * Sets whether this bottom sheet should skip the collapsed state when it is being hidden + * after it is expanded once. Setting this to true has no effect unless the sheet is hideable. + * + * @param skipCollapsed True if the bottom sheet should skip the collapsed state. + * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed + */ + public void setSkipCollapsed(boolean skipCollapsed) { + mSkipCollapsed = skipCollapsed; + } + + /** + * Sets whether this bottom sheet should skip the collapsed state when it is being hidden + * after it is expanded once. + * + * @return Whether the bottom sheet should skip the collapsed state. + * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed + */ + public boolean getSkipCollapsed() { + return mSkipCollapsed; + } + + /** + * Sets a callback to be notified of bottom sheet events. + * + * @param callback The callback to notify when bottom sheet events occur. + */ + public void setBottomSheetCallback(BottomSheetCallback callback) { + mCallback = callback; + } + + /** + * Sets the state of the bottom sheet. The bottom sheet will transition to that state with + * animation. + * + * @param state One of {@link #STATE_COLLAPSED}, {@link #STATE_EXPANDED}, or + * {@link #STATE_HIDDEN}. + */ + public final void setState(final @State int state) { + if (state == mState) { + return; + } + if (mViewRef == null) { + // The view is not laid out yet; modify mState and let onLayoutChild handle it later + if (state == STATE_COLLAPSED || state == STATE_EXPANDED || + (mHideable && state == STATE_HIDDEN)) { + mState = state; + } + return; + } + final V child = mViewRef.get(); + if (child == null) { + return; + } + // Start the animation; wait until a pending layout if there is one. + ViewParent parent = child.getParent(); + if (parent != null && parent.isLayoutRequested() && ViewCompat.isAttachedToWindow(child)) { + child.post(new Runnable() { + @Override + public void run() { + startSettlingAnimation(child, state); + } + }); + } else { + startSettlingAnimation(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; + } + + void setStateInternal(@State int state) { + if (mState == state) { + return; + } + mState = state; + View bottomSheet = mViewRef.get(); + if (bottomSheet != null && mCallback != null) { + mCallback.onStateChanged(bottomSheet, state); + } + } + + private void reset() { + mActivePointerId = ViewDragHelper.INVALID_POINTER; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + boolean shouldHide(View child, float yvel) { + if (mSkipCollapsed) { + return true; + } + if (child.getTop() < mMaxOffset) { + // It should not hide, but collapse. + return false; + } + final float newTop = child.getTop() + yvel * HIDE_FRICTION; + return Math.abs(newTop - mMaxOffset) / (float) mPeekHeight > HIDE_THRESHOLD; + } + + @VisibleForTesting + View findScrollingChild(View view) { + if (ViewCompat.isNestedScrollingEnabled(view)) { + return view; + } + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View scrollingChild = findScrollingChild(group.getChildAt(i)); + if (scrollingChild != null) { + return scrollingChild; + } + } + } + return null; + } + + private float getYVelocity() { + mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + return mVelocityTracker.getYVelocity(mActivePointerId); + } + + void startSettlingAnimation(View child, int state) { + int top; + if (state == STATE_COLLAPSED) { + top = mMaxOffset; + } else if (state == STATE_EXPANDED) { + top = mMinOffset; + } else if (mHideable && state == STATE_HIDDEN) { + top = mParentHeight; + } else { + throw new IllegalArgumentException("Illegal state argument: " + state); + } + if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) { + setStateInternal(STATE_SETTLING); + ViewCompat.postOnAnimation(child, new SettleRunnable(child, state)); + } else { + setStateInternal(state); + } + } + + private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() { + + @Override + public boolean tryCaptureView(View child, int pointerId) { + if (mState == STATE_DRAGGING) { + return false; + } + if (mTouchingScrollingChild) { + return false; + } + if (mState == STATE_EXPANDED && mActivePointerId == pointerId) { + View scroll = mNestedScrollingChildRef.get(); + if (scroll != null && scroll.canScrollVertically(-1)) { + // Let the content scroll up + return false; + } + } + return mViewRef != null && mViewRef.get() == child; + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + dispatchOnSlide(top); + } + + @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) { // Moving up + top = mMinOffset; + targetState = STATE_EXPANDED; + } else if (mHideable && shouldHide(releasedChild, yvel)) { + top = mParentHeight; + targetState = STATE_HIDDEN; + } else if (yvel == 0.f) { + int currentTop = releasedChild.getTop(); + if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) { + top = mMinOffset; + targetState = STATE_EXPANDED; + } else { + top = mMaxOffset; + targetState = STATE_COLLAPSED; + } + } else { + top = mMaxOffset; + targetState = STATE_COLLAPSED; + } + if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) { + setStateInternal(STATE_SETTLING); + ViewCompat.postOnAnimation(releasedChild, + new SettleRunnable(releasedChild, targetState)); + } else { + setStateInternal(targetState); + } + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return MathUtils.clamp(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset); + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + return child.getLeft(); + } + + @Override + public int getViewVerticalDragRange(View child) { + if (mHideable) { + return mParentHeight - mMinOffset; + } else { + return mMaxOffset - mMinOffset; + } + } + }; + + void dispatchOnSlide(int top) { + View bottomSheet = mViewRef.get(); + if (bottomSheet != null && mCallback != null) { + if (top > mMaxOffset) { + mCallback.onSlide(bottomSheet, (float) (mMaxOffset - top) / + (mParentHeight - mMaxOffset)); + } else { + mCallback.onSlide(bottomSheet, + (float) (mMaxOffset - top) / ((mMaxOffset - mMinOffset))); + } + } + } + + @VisibleForTesting + int getPeekHeightMin() { + return mPeekHeightMin; + } + + 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 AbsSavedState { + @State + final int state; + + public SavedState(Parcel source) { + this(source, null); + } + + public SavedState(Parcel source, ClassLoader loader) { + super(source, loader); + //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 Creator CREATOR = new ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + @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 BottomSheetBehavior 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) behavior; + } + +} From f25e8afc92935a1903abf45a94858c6b5453ae70 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 4 Jan 2018 17:23:17 +0200 Subject: [PATCH 2/3] Add the ability to open a bottom sheet to any height --- .../tools/ExtendedBottomSheetBehavior.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java b/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java index 0dffcc3c9c..193cc2908a 100644 --- a/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java +++ b/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java @@ -125,6 +125,8 @@ public class ExtendedBottomSheetBehavior extends CoordinatorLayo private static final float HIDE_FRICTION = 0.1f; + private static final float MIN_VELOCITY_FOR_SLIDE = 2000; + private float mMaximumVelocity; private int mPeekHeight; @@ -393,8 +395,13 @@ public class ExtendedBottomSheetBehavior extends CoordinatorLayo int top; int targetState; if (mLastNestedScrollDy > 0) { - top = mMinOffset; - targetState = STATE_EXPANDED; + if (Math.abs(getYVelocity()) > MIN_VELOCITY_FOR_SLIDE) { + top = mMinOffset; + targetState = STATE_EXPANDED; + } else { + top = child.getTop(); + targetState = STATE_DRAGGING; + } } else if (mHideable && shouldHide(child, getYVelocity())) { top = mParentHeight; targetState = STATE_HIDDEN; @@ -408,8 +415,13 @@ public class ExtendedBottomSheetBehavior extends CoordinatorLayo targetState = STATE_COLLAPSED; } } else { - top = mMaxOffset; - targetState = STATE_COLLAPSED; + if (Math.abs(getYVelocity()) > MIN_VELOCITY_FOR_SLIDE) { + top = mMaxOffset; + targetState = STATE_COLLAPSED; + } else { + top = child.getTop(); + targetState = STATE_DRAGGING; + } } if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) { setStateInternal(STATE_SETTLING); @@ -808,24 +820,24 @@ public class ExtendedBottomSheetBehavior extends CoordinatorLayo } /** - * A utility function to get the {@link BottomSheetBehavior} associated with the {@code view}. + * A utility function to get the {@link ExtendedBottomSheetBehavior} associated with the {@code view}. * - * @param view The {@link View} with {@link BottomSheetBehavior}. - * @return The {@link BottomSheetBehavior} associated with the {@code view}. + * @param view The {@link View} with {@link ExtendedBottomSheetBehavior}. + * @return The {@link ExtendedBottomSheetBehavior} associated with the {@code view}. */ @SuppressWarnings("unchecked") - public static BottomSheetBehavior from(V view) { + public static ExtendedBottomSheetBehavior 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)) { + if (!(behavior instanceof ExtendedBottomSheetBehavior)) { throw new IllegalArgumentException( "The view is not associated with BottomSheetBehavior"); } - return (BottomSheetBehavior) behavior; + return (ExtendedBottomSheetBehavior) behavior; } } From 3f437064c9430b3780787d7b7fb547a8c1f3c2fa Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 4 Jan 2018 18:19:57 +0200 Subject: [PATCH 3/3] Add custom bottom sheet state representing manual opening --- .../widgets/tools/ExtendedBottomSheetBehavior.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java b/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java index 193cc2908a..ee01299b40 100644 --- a/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java +++ b/OsmAnd/src/net/osmand/plus/widgets/tools/ExtendedBottomSheetBehavior.java @@ -104,11 +104,16 @@ public class ExtendedBottomSheetBehavior extends CoordinatorLayo */ public static final int STATE_HIDDEN = 5; + /** + * The bottom sheet is manually opened to a certain height. + */ + public static final int STATE_MANUALLY_MOVED = 6; + /** * @hide */ @RestrictTo(LIBRARY_GROUP) - @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN}) + @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN, STATE_MANUALLY_MOVED}) @Retention(RetentionPolicy.SOURCE) public @interface State { } @@ -400,7 +405,7 @@ public class ExtendedBottomSheetBehavior extends CoordinatorLayo targetState = STATE_EXPANDED; } else { top = child.getTop(); - targetState = STATE_DRAGGING; + targetState = STATE_MANUALLY_MOVED; } } else if (mHideable && shouldHide(child, getYVelocity())) { top = mParentHeight; @@ -420,7 +425,7 @@ public class ExtendedBottomSheetBehavior extends CoordinatorLayo targetState = STATE_COLLAPSED; } else { top = child.getTop(); - targetState = STATE_DRAGGING; + targetState = STATE_MANUALLY_MOVED; } } if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {