Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android - How to add parallax effect as bottom sheet covers another view

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!

like image 348
Patrick Grayson Avatar asked Aug 28 '17 21:08

Patrick Grayson


People also ask

Is parallax scrolling outdated?

It's an outdated attention-goosing technique that most designers will probably be embarrassed to look back on. Remember about five years ago, when the new hotness in interaction design was to have flashy layers in your website scroll at different speeds, creating a faux-3D effect?

Why does parallax not work on mobile?

When you use other browsers on any touch device, parallax scrolling will be overwritten for compatibility reasons. As a result, you will only see a static image instead of your parallax.

What is a parallax scrolling background?

What is a parallax effect? Parallax effects involve a website's background moving at a different speed than the foreground content. This visual technique creates an illusion of depth which leads to a faux-3D effect upon scroll.

Is parallax scrolling good?

Parallax scrolling gives web designers a unique opportunity to introduce a sense of depth into their design to keep visitors engaged. Well-crafted parallax website designs can easily help you stand out from the crowd and create a lasting impression for your visitors.


2 Answers

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>
like image 170
Patrick Grayson Avatar answered Nov 10 '22 07:11

Patrick Grayson


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>
like image 4
HondaGuy Avatar answered Nov 10 '22 07:11

HondaGuy