Android Support BottomSheetBehavior additional anchored state

11,449

Solution 1

BIG UPDATE Because there were like 4 or 5 questions about the same topic BUT with DIFFERENT requirements and I tried to answer all of them. A non-polite admin deleted/closed them making me create a ticket for each one and changing them to avoid "copy-paste". I will give you a link to the full answer in where you can find all the explanation about how to get full behavior like Google Maps.


Answering your questions

How could I use BottomSheetBehavior and get this additional anchored state?

You can do it by modifying default BottomSheetBehavior adding one more stat with the following steps:

  1. Create a Java class and extend it from CoordinatorLayout.Behavior<V>
  2. Copy paste code from the default BottomSheetBehavior file to your new one.
  3. Modify the method clampViewPositionVertical with the following code:
    @Override
    public int clampViewPositionVertical(View child, int top, int dy) {
        return constrain(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
    }

    int constrain(int amount, int low, int high) {
        return amount < low ? low : (amount > high ? high : amount);
    }
  1. Add a new state

    public static final int STATE_ANCHOR_POINT = X;

  2. Modify the next methods: onLayoutChild, onStopNestedScroll, BottomSheetBehavior<V> from(V view) and setState (optional)



I'm going to add those modified methods and a link to the example project

public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
    // First let the parent lay it out
    if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {
        if (ViewCompat.getFitsSystemWindows(parent) &&
                !ViewCompat.getFitsSystemWindows(child)) {
            ViewCompat.setFitsSystemWindows(child, true);
        }
        parent.onLayoutChild(child, layoutDirection);
    }
    // Offset the bottom sheet
    mParentHeight = parent.getHeight();
    mMinOffset = Math.max(0, mParentHeight - child.getHeight());
    mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);

    //if (mState == STATE_EXPANDED) {
    //    ViewCompat.offsetTopAndBottom(child, mMinOffset);
    //} else if (mHideable && mState == STATE_HIDDEN...
    if (mState == STATE_ANCHOR_POINT) {
        ViewCompat.offsetTopAndBottom(child, mAnchorPoint);
    } else 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);
    }
    if (mViewDragHelper == null) {
        mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
    }
    mViewRef = new WeakReference<>(child);
    mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
    return true;
}


public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
    if (child.getTop() == mMinOffset) {
        setStateInternal(STATE_EXPANDED);
        return;
    }
    if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) {
        return;
    }
    int top;
    int targetState;
    if (mLastNestedScrollDy > 0) {
        //top = mMinOffset;
        //targetState = STATE_EXPANDED;
        int currentTop = child.getTop();
        if (currentTop > mAnchorPoint) {
            top = mAnchorPoint;
            targetState = STATE_ANCHOR_POINT;
        }
        else {
            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;
        int currentTop = child.getTop();
        if (currentTop > mAnchorPoint) {
            top = mMaxOffset;
            targetState = STATE_COLLAPSED;
        }
        else {
            top = mAnchorPoint;
            targetState = STATE_ANCHOR_POINT;
        }
    }
    if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
        setStateInternal(STATE_SETTLING);
        ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
    } else {
        setStateInternal(targetState);
    }
    mNestedScrolled = false;
}

public final void setState(@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
        /**
         * New behavior (added: state == STATE_ANCHOR_POINT ||)
         */
        if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
                state == STATE_ANCHOR_POINT ||
                (mHideable && state == STATE_HIDDEN)) {
            mState = state;
        }
        return;
    }
    V child = mViewRef.get();
    if (child == null) {
        return;
    }
    int top;
    if (state == STATE_COLLAPSED) {
        top = mMaxOffset;
    } else if (state == STATE_ANCHOR_POINT) {
        top = mAnchorPoint;
    } else if (state == STATE_EXPANDED) {
        top = mMinOffset;
    } else if (mHideable && state == STATE_HIDDEN) {
        top = mParentHeight;
    } 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));
    }
}


public static <V extends View> BottomSheetBehaviorGoogleMapsLike<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 BottomSheetBehaviorGoogleMapsLike)) {
        throw new IllegalArgumentException(
                "The view is not associated with BottomSheetBehaviorGoogleMapsLike");
    }
    return (BottomSheetBehaviorGoogleMapsLike<V>) behavior;
}



You can even use callbacks with behavior.setBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {....

And here is how its looks like
[CustomBottomSheetBehavior]

Solution 2

there are some easy way to changing BottomSheetBehavior change state with anchor FloatingActionButton

<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

  <android.support.v7.widget.CardView
    android:id="@+id/bottom_sheet"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:behavior_hideable="false"
    app:behavior_peekHeight="44dp"
    app:cardCornerRadius="0dp"
    app:cardElevation="5dp"
    app:layout_behavior="@string/bottom_sheet_behavior">

</android.support.v7.widget.CardView>

<android.support.design.widget.FloatingActionButton
    android:id="@+id/request_show_fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    android:src="@drawable/fab_requests"
    app:layout_anchor="@id/bottom_sheet"
    app:layout_anchorGravity="top|end" />

now you can change state click on FloatingActionButton

bottomSheetBehavior = BottomSheetBehavior.from(bottom_sheet);

    request_show_fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
            } else {
                bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);

            }
        }
    });
Share:
11,449
Jeff Lockhart
Author by

Jeff Lockhart

Updated on June 17, 2022

Comments

  • Jeff Lockhart
    Jeff Lockhart almost 2 years

    I have been using the AndroidSlidingUpPanel library in my app. With versions of the Android Design Support Library since 23.1.1, this breaks some things in my layout. Since the newest versions introduce the BottomSheetBehavior, I'm looking to replace the AndroidSlidingUpPanel library and use BottomSheetBehavior instead. However, BottomSheetBehavior only has 3 states, hidden, collapsed, and expanded (as well as 2 intermediate states dragging and settling). AndroidSlidingUpPanel additionally has the anchored state, which is a state the panel snaps to in between collapsed and expanded. How could I use BottomSheetBehavior and get this additional anchored state?

    Google's Maps app has this behavior for example.

    Hidden:

    Collapsed:

    Dragging (between collapsed and anchored):

    Anchored:

    Dragging (between anchored and expanded):

    Expanded:

    There are some parallax effects going on with an optional image sliding up over the map in the anchored state when locations have them. And when becoming fully expanded, the location name becomes the action bar title. I'd eventually be interested in achieving something similar as well.

    My first instinct is that the anchored state is in fact the expanded state, with the empty space above the panel, where the map is still visible, being a transparent portion of the view. Then the dragging between the anchored and expanded states is just scrolling the contents of the panel view itself.

    This is validated by the fact that while in the anchored state you can continue to scroll the panel up by swiping the visible map region above the panel. This invisible portion of the view must expand into its area (as the optional images visibly do) while swiping up from the collapsed state though, as it is not possible to slide the panel up from the map in the collapsed state. I suppose I could go this route but wanted to see if there were any better approaches out there.

  • Jeff Lockhart
    Jeff Lockhart almost 8 years
    Thanks, I'll look into this when I get to implementing that final state. It'd be nice to not have to copy/paste code. Would be better if it was possible to subclass in some way, but probably needs code not visible in subclass.
  • MiguelHincapieC
    MiguelHincapieC almost 8 years
    Yeah I tried to subclass but was a pain in the %$#. Now I'm working in parallax image effect like google maps.
  • MiguelHincapieC
    MiguelHincapieC almost 8 years
    @JeffLockhart I got it! now its working with image parallax effect and toolbars animations...I'm only missing the color that takes the toolbar when you keep sliding up :D
  • MiguelHincapieC
    MiguelHincapieC almost 8 years
    Hi there, did you find my answer helpful, or do you need more help? :)
  • Jeff Lockhart
    Jeff Lockhart almost 8 years
    yes, thank you. We are still working on finalizing the design for this behavior, after which I'll be able to finish implementing it in code. This information is helpful, thank you.
  • kjanderson2
    kjanderson2 almost 7 years
    @MiguelHincapieC I have cloned your repo and am working on getting a similar problem solved. However, I need my view with bottomsheetbehavior to be a LinearLayout with a NestedScrollView within. However, when I do this the onStopNestedScroll never gets called so the anchor state is never hit. Do you have any ideas for this?
  • MiguelHincapieC
    MiguelHincapieC almost 7 years
    Hi @kjanderson2 using a LinearLayout shouldn't be problem. Did you check the XML when you define behavior?
  • kjanderson2
    kjanderson2 almost 7 years
    @MiguelHincapieC I did and it wasn't working. However, I have since solved that problem by rearranging my layouts. I am now having a problem where when the drawer is at the anchor position, it is still draggable by dragging from areas above the sheet. So I am no longer able to interact with the view beneath my bottom sheet because the area that the sheet isn't currently occupying (between anchor and expanded) is still draggable. Have you encountered this? Do you have any ideas for a fix?
  • MiguelHincapieC
    MiguelHincapieC almost 7 years
    @kjanderson2 I don't understand very well the problem, can you share a gif the behavior?
  • kjanderson2
    kjanderson2 almost 7 years
    @MiguelHincapieC It's okay. I got it working. Thanks!