8

Using BottomSheetBehavior from the google design library, it looks like the default behavior is for the bottom sheet to "cover" other views in the same CoordinatorLayout as it expands. I can anchor something like a FAB (or other view with an appropriately defined CoordinatorLayout.Behavior) to the top of the sheet and have it slide up as the sheet expands, which is nice, but what I want is to have a view "collapse" as the bottom sheet expands, showing a parallax effect.

This effect in Google Maps is similar to what I'm looking for; it starts as a parallax effect, and then switches back to just having the bottom sheet "cover" the map once a certain scroll position is reached:

enter image description here

One thing I tried (though I suspected from the start it wouldn't work), was setting the upper view's height programmatically in the onSlide call of my BottomSheetBehavior.BottomSheetCallback. This was somewhat successful, but the movement wasn't nearly as smooth as in Google Maps.

If anyone has an idea how the effect is accomplished I would appreciate it a lot!

Patrick Grayson
  • 548
  • 3
  • 17

2 Answers2

14

After a bit more experimenting/research I realized from this post How to make custom CoordinatorLayout.Behavior with parallax scrolling effect for google MapView? that a big part of my problem was not understanding the parallax effect, which translates views rather than shrinking them. Once I realized that, it was trivial to create a custom behavior that would apply the parallax to my main view when the bottom sheet expanded:

public class CollapseBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V>{


  public CollapseBehavior(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  @Override
  public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
    if (isBottomSheet(dependency)) {
        BottomSheetBehavior behavior = ((BottomSheetBehavior) ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior());

        int peekHeight = behavior.getPeekHeight();
        // The default peek height is -1, which 
        // gets resolved to a 16:9 ratio with the parent
        final int actualPeek = peekHeight >= 0 ? peekHeight : (int) (((parent.getHeight() * 1.0) / (16.0)) * 9.0);
        if (dependency.getTop() >= actualPeek) {
            // Only perform translations when the 
            // view is between "hidden" and "collapsed" states
            final int dy = dependency.getTop() - parent.getHeight();
            ViewCompat.setTranslationY(child, dy/2);
            return true;
        }
    }

    return false;
  }

  private static boolean isBottomSheet(@NonNull View view) {
    final ViewGroup.LayoutParams lp = view.getLayoutParams();
    if (lp instanceof CoordinatorLayout.LayoutParams) {
        return ((CoordinatorLayout.LayoutParams) lp)
                .getBehavior() instanceof BottomSheetBehavior;
    }
    return false;
  }


}

Then in my layout XML, I set the app:layout_behavior of my main view to be com.mypackage.CollapseBehavior and the app:layout_anchor to be my bottom sheet view so that the onDependentViewChanged callback would trigger. This effect was much smoother than trying to resize the view. I suspect returning to my initial strategy of using a BottomSheetBehavior.BottomSheetCallback would also work similarly to this solution.

Edit: per request, the relevant XML is below. I add a MapFragment into @+id/map_container at runtime, though this should also work with anything you drop into that container like a static image. The LocationListFragment could likewise be replaced with any view or fragment, so long as it still has the BottomSheetBehavior

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/fragment_coordinator">
        <FrameLayout
            android:id="@+id/map_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            app:layout_anchor="@+id/list_container"
            app:layout_behavior="com.mypackage.behavior.CollapseBehavior"/>

        <fragment
            android:name="com.mypackage.fragment.LocationListFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/list_container"
            app:layout_behavior="android.support.design.widget.BottomSheetBehavior"/>


    </android.support.design.widget.CoordinatorLayout>
Rahul
  • 3,293
  • 2
  • 31
  • 43
Patrick Grayson
  • 548
  • 3
  • 17
  • I know this was a while ago, but do you mind posting your xml for this one too? – Pythogen Sep 22 '17 at 00:16
  • 2
    No probelm! I've added it – Patrick Grayson Sep 22 '17 at 15:07
  • Thank's for this post, It's helpful. but I have facing an issue, when I expend bottom sheet then logo of google is hiding can we resolve it? – Rashpal Singh Apr 06 '19 at 04:20
  • @RashpalSingh I'm not entirely sure, but if you are using maps sdk v2 or later then this may be achievable using `GoogleMap.setPadding()`, which will move the logo and other parts of the map view without moving the map itself (see here https://stackoverflow.com/questions/6923472/relocate-google-logo-in-mapview). This will require a lot of changes to my code above, since you'll need a way to trigger the padding change from `onDependentViewChanged`, but increasing the padding as the bottom sheet expands should keep the logo visible – Patrick Grayson Apr 08 '19 at 22:13
4

Patrick Grayson's post was very helpful. In my case though, I did need something that resized the map. I adopted the solution above to resize instead of translate. Perhaps others may be looking for a similar solution.

public class CollapseBehavior<V extends ViewGroup> extends CoordinatorLayout.Behavior<V> {

private int pixels = NO_RESIZE_BUFFER; // default value, in case getting a value from resources, bites the dust.

private static final int NO_RESIZE_BUFFER = 200; //The amount of dp to not have the bottom sheet ever push away.

public CollapseBehavior(Context context, AttributeSet attrs)
{
    super(context, attrs);
    pixels = (int)convertDpToPixel(NO_RESIZE_BUFFER,context);
}

@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
    // child is the map
    // dependency is the bottomSheet
    if(isBottomSheet(dependency))
    {
        BottomSheetBehavior behavior = ((BottomSheetBehavior) ((CoordinatorLayout.LayoutParams)dependency.getLayoutParams()).getBehavior());

        int peekHeight;
        if (behavior != null) {
            peekHeight = behavior.getPeekHeight();
        }
        else
            return true;

        if(peekHeight > 0) { // Dodge the case where the sheet is hidden.

            if (dependency.getTop() >= peekHeight) { // Otherwise we'd completely overlap the map

                if(dependency.getTop() >= pixels) { // On resize when we have more than our NO_RESIZE_BUFFER of dp left.
                    if(dependency.getTop() > 0) { // Don't want to map to be gone completely.
                        CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
                        params.height = dependency.getTop();
                        child.setLayoutParams(params);
                    }
                    return true;
                }
            }
        }
    }
    return false;
}

private static float convertDpToPixel(float dp, Context context)
{
    float densityDpi = context.getResources().getDisplayMetrics().densityDpi;
    return dp * (densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}

private static boolean isBottomSheet(@NonNull View view)
{
    final ViewGroup.LayoutParams lp = view.getLayoutParams();
    if(lp instanceof CoordinatorLayout.LayoutParams)
    {
        return ((CoordinatorLayout.LayoutParams) lp).getBehavior() instanceof BottomSheetBehavior;
    }
    return false;
}
}

And the layout...

<FrameLayout
    android:id="@+id/flMap"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="top"
    android:layout_margin="0dp"
    app:layout_anchor="@+id/persistentBottomSheet"
    app:layout_behavior="com.yoursite.yourapp.CollapseBehavior">

    <fragment
        android:id="@+id/mapDirectionSummary"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.yoursite.yourapp.YourActivity" />

</FrameLayout>

<android.support.constraint.ConstraintLayout
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="wrap_content"
android:id="@+id/persistentBottomSheet"
app:behavior_peekHeight="@dimen/bottom_sheet_peek_height"
app:behavior_hideable="false"
    app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
    tools:context="com.yoursite.yourapp.YourActivity">

<!-- Whatever you want on the bottom sheet. -->

</android.support.constraint.ConstraintLayout>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="8dp"
        app:cardElevation="8dp"
        app:cardBackgroundColor="#324">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:background="?attr/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
            app:popupTheme="@style/Theme.AppCompat.Light">

            <EditText
                android:id="@+id/txtSearch"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:ems="10"
                android:inputType="text"
                android:maxLines="1" />


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


</LinearLayout>
Rahul
  • 3,293
  • 2
  • 31
  • 43
HondaGuy
  • 1,251
  • 12
  • 29