animateLayoutChanges="true" in BottomSheetView showing unexpected behaviour

AndroidAndroid LayoutAndroid FragmentsAnimationBottom Sheet

Android Problem Overview


I have a BottomSheetView which has animateLayoutChanges="true". Initially it shows up fine. But if change the visibility of a view (inside BottomSheetView) from GONE to VISIBLE, the app messes up calculations and my BottomSheetView moves to the top of the screen. i have tried setting layout_gravity=bottom at the root of the BottomSheetView layout. But no success.

Here I have the image of my BottomSheetView before changing the visibility of any view. (Click image for full size)

enter image description here

After I change the visibility of a view (GONE to VISIBLE or VISIBLE to GONE), my BottomSheetView moves to the top. (Click image for full size)

enter image description here

I guess, Android is messing up while making calculations about the measurement of view width and height. Any way to solve this??

I also tried to make my BottomSheetView extend fully to match the parent view, but somehow that is making the height of the BottomSheetView longer than the phone screen and in-tun creating scrolling issues.

Expected solutions:

1> Prevent BottomSheetView from changing its position even when the visibility of a view is changed.

OR

2>Make the BottomSheetView match parent so that it does not look bad after messing up with the calculations.

Android Solutions


Solution 1 - Android

The BottomSheetBehavior does not work well with LayoutTransition (animateLayoutChanges="true") for now. I will work on a fix.

For now, you can use Transition instead. Something like this will fade the view inside and animate the size of the bottom sheet.

ViewGroup bottomSheet = ...;
View hidingView = ...;

TransitionManager.beginDelayedTransition(bottomSheet);
hidingView.setVisibility(View.GONE);

You can refer to Applying a Transition for more information including how to customize the animation.

Solution 2 - Android

I was running into the same issue and determined to find a fix. I was able to find the underlying cause but unfortunately I do not see a great fix at the moment.

The Cause: The problem occurs between the bottomsheet behavior and the LayoutTransition. When the LayoutTransition is created, it creates a OnLayoutChangeListener on the view so that it can capture its endValues and setup an animator with the proper values. This OnLayoutChangeListener is triggered in the bottomSheetBehavior's onLayout() call when it first calls parent.onLayout(child). The parent will layout the child as it normally would, ignoring any offsets that the behavior would change later. The problem lies here. The values of the view at this point are captured by the OnLayoutChangeListener and stored in the animator. When the animation runs, it will animate to these values, not to where your behavior defines. Unfortunately, the LayoutTransition class does not give us access to the animators to allow updating of the end values.

The Fix: Currently, I don't see an elegant fix that involves LayoutTransitions. I am going to submit a bug for a way to access and update LayoutTransition animators. For now you can disable any layoutTransition on the parent container using layoutTransition.setAnimateParentHierarchy(false). Then you can animate the change yourself. I'll update my answer with a working example as soon as I can.

Solution 3 - Android

The question was asked more than two years ago, but unfortunately the problem persists.

I finally got a solution to keep the call to the addView and removeView functions in a BottomSheet, while having animateLayoutChanges="true".

BottomSheetBehavior cannot calculate the correct height when it changes, so the height must remain the same. To do this, I set the height of the BottomSheet to match_parent and divide it into two children: the content and a Space that changes height according to the height of the content.

To best mimic the true behavior of a BottomSheet, you also need to add a TouchToDismiss view that darkens the background when the BottomSheet is extended but also to close the BottomSheet when the user presses outside the content.

Here's the code:

activity.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/show_bottom_sheet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Show bottom sheet"/>

    <View
        android:id="@+id/touch_to_dismiss"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clickable="true"
        android:background="#9000"/>

    <LinearLayout
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">

        <Space
            android:id="@+id/space"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_weight="1"/>

        <LinearLayout
            android:id="@+id/bottom_sheet_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:animateLayoutChanges="true">

            <Button
                android:id="@+id/add_or_remove_another_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Add another view"/>

            <TextView
                android:id="@+id/another_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Another view"/>

        </LinearLayout>

    </LinearLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

activity.java

BottomSheetBehavior bottomSheetBehavior;
View touchToDismiss;
LinearLayout bottomSheet;
Button showBottomSheet;
Space space;
LinearLayout bottomSheetContent;
Button addOrRemoveAnotherView;
TextView anotherView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    touchToDismiss = findViewById(R.id.touch_to_dismiss);
    touchToDismiss.setVisibility(View.GONE);
    touchToDismiss.setOnClickListener(this);

    bottomSheet = findViewById(R.id.bottom_sheet);
    
    bottomSheetBehavior = BottomSheetBehavior.from(bottomSheet);
    bottomSheetBehavior.setPeekHeight(0);
    bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
    bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
        @Override
        public void onStateChanged(@NonNull View bottomSheet, int newState) {
            if (newState == BottomSheetBehavior.STATE_HIDDEN || newState == BottomSheetBehavior.STATE_COLLAPSED) {
                touchToDismiss.setVisibility(View.GONE);
            }else {
                touchToDismiss.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            touchToDismiss.setAlpha(getRealOffset());
        }
    });
    
    showBottomSheet = findViewById(R.id.show_bottom_sheet);
    showBottomSheet.setOnClickListener(this);
    
    space = findViewById(R.id.space);

    bottomSheetContent = findViewById(R.id.bottom_sheet_content);

    addOrRemoveAnotherView = findViewById(R.id.add_or_remove_another_view);
    addOrRemoveAnotherView.setOnClickListener(this);
    
    anotherView = findViewById(R.id.another_view);
    bottomSheetContent.removeView(anotherView);
}

@Override
public void onClick(View v) {
    if (v == showBottomSheet)
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
    else if (v == addOrRemoveAnotherView) {
        if (anotherView.getParent() == null)
            bottomSheetContent.addView(anotherView);
        else
            bottomSheetContent.removeView(anotherView);
    }
    else if (v == touchToDismiss)
        bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}

/**
 * Since the height does not change and remains at match_parent, it is required to calculate the true offset.
 * @return Real offset of the BottomSheet content.
 */
public float getRealOffset() {
    float num = (space.getHeight() + bottomSheetContent.getHeight()) - (bottomSheet.getY() + space.getHeight());

    float den = bottomSheetContent.getHeight();

    return (num / den);
}

This is the result obtained with this code: final result

Hopefully it will be useful to someone since the problem is still there!

Solution 4 - Android

In the BottomSheetDialog default layout (design_bottom_sheet_dialog) there is a TOP gravity on the dialog's design_bottom_sheet FrameLayout:

 android:layout_gravity="center_horizontal|top"

I don't really know why on BottomSheetDialog gravity is top.

You need to create the same layout file (same content and name) in your project and replace this line with:

android:layout_gravity="center_horizontal|bottom"

Solution 5 - Android

I use this and I got to keep the animations!

 val transition = LayoutTransition()
 transition.setAnimateParentHierarchy(false)
 {Parent View which has animateLayoutChanges="true" }.layoutTransition = transition

Solution 6 - Android

In our case, we were displaying a progress bar on a button component. This was hiding the button text and displaying progress bar. This was causing a jump in bottom sheet fragment. Using Invisible instead of Gone fixed the problem.

Solution 7 - Android

If your view allows, make the visibility INVISIBLE instead of GONE.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSrujan BaraiView Question on Stackoverflow
Solution 1 - AndroidYuichi ArakiView Answer on Stackoverflow
Solution 2 - Androidartyoda21View Answer on Stackoverflow
Solution 3 - AndroidCharles AnnicView Answer on Stackoverflow
Solution 4 - AndroidbitvaleView Answer on Stackoverflow
Solution 5 - AndroidAlaa AbuZarifaView Answer on Stackoverflow
Solution 6 - AndroidMuratView Answer on Stackoverflow
Solution 7 - Androidarihant singhiView Answer on Stackoverflow