Android Support BottomSheetBehavior additional anchored state
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:
- Create a Java class and extend it from
CoordinatorLayout.Behavior<V>
- Copy paste code from the default
BottomSheetBehavior
file to your new one. - 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);
}
-
Add a new state
public static final int STATE_ANCHOR_POINT = X;
-
Modify the next methods:
onLayoutChild
,onStopNestedScroll
,BottomSheetBehavior<V> from(V view)
andsetState
(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
[]
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);
}
}
});
Jeff Lockhart
Updated on June 17, 2022Comments
-
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 almost 8 yearsThanks, 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 almost 8 yearsYeah I tried to subclass but was a pain in the %$#. Now I'm working in parallax image effect like google maps.
-
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 almost 8 yearsHi there, did you find my answer helpful, or do you need more help? :)
-
Jeff Lockhart almost 8 yearsyes, 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 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 almost 7 yearsHi @kjanderson2 using a LinearLayout shouldn't be problem. Did you check the XML when you define behavior?
-
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 almost 7 years@kjanderson2 I don't understand very well the problem, can you share a gif the behavior?
-
kjanderson2 almost 7 years@MiguelHincapieC It's okay. I got it working. Thanks!