How to mimic Google Maps' bottom-sheet 3 phases behavior?

37,724

Solution 1

Note: Read the edits at the bottom


OK, I've found a way to do it, but I had to change the code of multiple classes, so that the bottom sheet would know of the state of the appBarLayout (expanded or not), and ignore scroll-up in case it's not expanded:

BottomSheetLayout.java

Added fields:

private AppBarLayout mAppBarLayout;
private OnOffsetChangedListener mOnOffsetChangedListener;
private int mAppBarLayoutOffset;

init() - added this:

    mOnOffsetChangedListener = new OnOffsetChangedListener() {
        @Override
        public void onOffsetChanged(final AppBarLayout appBarLayout, final int verticalOffset) {
            mAppBarLayoutOffset = verticalOffset;
        }
    };

Added function to set the appBarLayout:

public void setAppBarLayout(final AppBarLayout appBarLayout) {
    if (mAppBarLayout == appBarLayout)
        return;
    if (mAppBarLayout != null)
        mAppBarLayout.removeOnOffsetChangedListener(mOnOffsetChangedListener);
    mAppBarLayout = appBarLayout;
    mAppBarLayout.addOnOffsetChangedListener(mOnOffsetChangedListener);
}

onDetachedFromWindow() - added this:

    if (mAppBarLayout != null)
        mAppBarLayout.removeOnOffsetChangedListener(mOnOffsetChangedListener);

onTouchEvent() - added this:

      ...
      if (bottomSheetOwnsTouch) {
        if (state == State.EXPANDED && scrollingDown && mAppBarLayout != null && mAppBarLayoutOffset != 0) {
            event.offsetLocation(0, sheetTranslation - getHeight());
            getSheetView().dispatchTouchEvent(event);
            return true;
        }
      ...

Those were the main changes. Now for what sets them:

MyFragment.java

onCreateView() - added this:

    mBottomSheetLayout.setAppBarLayout((AppBarLayout) view.findViewById(R.id.appbar));

I also added this function:

 public void setBottomSheetLayout(final BottomSheetLayout bottomSheetLayout) {
    mBottomSheetLayout = bottomSheetLayout;
}

Now this is how the activity tells the fragment about the appBarLayout:

            final MyFragment myFragment = new MyFragment();
            myFragment.setBottomSheetLayout(bottomSheetLayout);
            myFragment.show(getSupportFragmentManager(), R.id.bottomsheet);

The project is now available on GitHub:

https://github.com/AndroidDeveloperLB/ThreePhasesBottomSheet

I hope it doesn't have any bugs.


The solution has bugs, sadly, so I won't mark this answer as the correct one:

  1. It only works well on Android 6 and above. Others have a weird behavior of showing the bottom sheet expanded for a tiny fraction of a time, each time when showing it.
  2. Orientation changes do not save the state of the scrolling at all, so I've disabled it.
  3. Rare issue of being able to scroll inside the bottom sheet's content while it's still collapsed (at the bottom)
  4. If a keyboard was shown before, the bottom sheet might get to be full screen when trying to peek.

If anyone can help with it, please do.


For issue #1, I've tried adding a fix by setting the visibility to INVISIBLE when the bottom sheet doesn't peek yet, but it doesn't always work, especially if a keyboard is shown.


For issue #1, I've found how to fix it, by just wrapping (in "fragment_my.xml") the CoordinatorLayout with any view that you wish to use (I used FrameLayout), and also put a full-sized view in it (I just put "View") , as such:

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!--This full sized view, together with the FrameLayout above, are used to handle some weird UI issues on pre-Android-6 -->
    <View
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <...CollapsingToolbarLayout 
    ...

It probably confused the bottomSheet when I had the CoordinatorLayout being its view. I've updated the project, but still, if there is any way to have a nicer solution, I'd like to know about it.


In recent months, Google has published its own bottomSheet class, but as I've found it has a lot of issues, so I can't even try it out.

Solution 2

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, but a non-polite admin deleted/closed them, making me create a ticket for each one and changing them to avoid "copy-paste" I will let 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 question

How to mimic Google Maps' bottom-sheet 3 phases behavior?

With support library 23.x.x+ you can do it by modifying the 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);
    }
    
  4. Add a new state:

    public static final int STATE_ANCHOR_POINT = X;
    
  5. 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.

And here is how its looks like
CustomBottomSheetBehavior

Share:
37,724

Related videos on Youtube

android developer
Author by

android developer

Really like to develop Android apps &amp; libraries on my spare time. Github website: https://github.com/AndroidDeveloperLB/ My spare time apps: https://play.google.com/store/apps/developer?id=AndroidDeveloperLB

Updated on July 08, 2022

Comments

  • android developer
    android developer almost 2 years

    Background

    I'm assigned to make a UI that behaves similarly to how Google Maps shows a bottom-sheet for a found result.

    It has three different phases:

    1. Bottom content. The upper area is still touchable and won't scroll anything at the bottom
    2. Full-screen content, while the upper area has a large header.
    3. Full-screen content, while the upper area has just the toolbar.

    Here's what I'm talking about on Google Maps:

    Enter image description here

    The problem

    Thing is, the bottom sheet isn't a part of the design library yet (though it was requested, here).

    Not only that, but the UI seems quite complex and needs handling of the toolbar on multiple phases.

    What I've tried

    I've found a good (enough) library for the bottom sheet (here), and added content to its fragment sample, to have about the same views as shown on material design samples (like here), to have a CollapsingToolbarLayout that will take care of phases 2+3.

    In the app I'm making, I also have to move an icon as you scroll, but I think that if I succeed with the rest, this should be easy. Here's the code:

    ###fragment_my.xml

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.design.widget.CoordinatorLayout
        android:id="@+id/main_content"
        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.support.design.widget.AppBarLayout
            android:id="@+id/appbar"
            android:layout_width="match_parent"
            android:layout_height="@dimen/detail_backdrop_height"
    
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
    
            <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/collapsing_toolbar"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
    
                app:contentScrim="?attr/colorPrimary"
                app:expandedTitleMarginEnd="64dp"
                app:expandedTitleMarginStart="48dp"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
    
                <ImageView
                    android:id="@+id/backdrop"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="centerCrop"
                    app:layout_collapseMode="parallax"/>
    
                <android.support.v7.widget.Toolbar
                    android:id="@+id/toolbar"
                    android:layout_width="match_parent"
                    android:layout_height="?attr/actionBarSize"
                    app:layout_collapseMode="pin"
                    app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>
            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>
    
        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical"
                android:paddingTop="24dp">
    
                <android.support.v7.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="@dimen/card_margin">
    
                    <LinearLayout
                        style="@style/Widget.CardContent"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content">
    
                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="Info"
                            android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
    
                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="@string/cheese_ipsum"/>
                    </LinearLayout>
                </android.support.v7.widget.CardView>
    
                <android.support.v7.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/card_margin"
                    android:layout_marginLeft="@dimen/card_margin"
                    android:layout_marginRight="@dimen/card_margin">
    
                    <LinearLayout
                        style="@style/Widget.CardContent"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content">
    
                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="Friends"
                            android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
    
                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="@string/cheese_ipsum"/>
                    </LinearLayout>
                </android.support.v7.widget.CardView>
    
                <android.support.v7.widget.CardView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/card_margin"
                    android:layout_marginLeft="@dimen/card_margin"
                    android:layout_marginRight="@dimen/card_margin">
    
                    <LinearLayout
                        style="@style/Widget.CardContent"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content">
    
                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="Related"
                            android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
    
                        <TextView
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:text="@string/cheese_ipsum"/>
                    </LinearLayout>
                </android.support.v7.widget.CardView>
            </LinearLayout>
        </android.support.v4.widget.NestedScrollView>
    
        <android.support.design.widget.FloatingActionButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/fab_margin"
            android:clickable="true"
            android:src="@android:drawable/ic_menu_send"
            app:layout_anchor="@id/appbar"
            app:layout_anchorGravity="bottom|right|end"/>
    
    </android.support.design.widget.CoordinatorLayout>
    

    ###MyFragment.java

    public class MyFragment extends BottomSheetFragment {
    
        @Nullable
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            final View view = inflater.inflate(R.layout.fragment_my, container, false);
            view.setMinimumHeight(getResources().getDisplayMetrics().heightPixels);
            CollapsingToolbarLayout collapsingToolbar = (CollapsingToolbarLayout) view.findViewById(R.id.collapsing_toolbar);
            collapsingToolbar.setTitle("AAA");
            final Toolbar toolbar = (Toolbar) view.findViewById(R.id.toolbar);
            final AppCompatActivity activity = (AppCompatActivity) getActivity();
            activity.setSupportActionBar(toolbar);
            activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
            //toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_mtrl_am_alpha);
            toolbar.setNavigationOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    NavUtils.navigateUpFromSameTask(getActivity());
                }
            });
            final ImageView imageView = (ImageView) view.findViewById(R.id.backdrop);
    
            Glide.with(this).load(R.drawable.cheese_1).centerCrop().into(imageView);
            return view;
        }
    }
    

    ###BottomSheetFragmentActivity.java

    public final class BottomSheetFragmentActivity extends AppCompatActivity {
    
        protected BottomSheetLayout bottomSheetLayout;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_bottom_sheet_fragment);
            bottomSheetLayout = (BottomSheetLayout) findViewById(R.id.bottomsheet);
            findViewById(R.id.bottomsheet_fragment_button).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    new MyFragment().show(getSupportFragmentManager(), R.id.bottomsheet);
                }
            });
            bottomSheetLayout.setShouldDimContentView(false);
            bottomSheetLayout.setPeekOnDismiss(true);
            bottomSheetLayout.setPeekSheetTranslation(200);
            bottomSheetLayout.setInterceptContentTouch(false);
            bottomSheetLayout.setDefaultViewTransformer(new BaseViewTransformer() {
                @Override
                public void transformView(final float translation, final float maxTranslation, final float peekedTranslation, final BottomSheetLayout parent, final View view) {
                    Log.d("AppLog", "translation:" + translation + " maxTranslation:" + maxTranslation + " peekedTranslation:" + peekedTranslation);
                }
            });
        }
    }
    

    It almost works well. The only problem is the transition from #3 back to #2:

    enter image description here

    The question

    What is wrong with the code? What can I do in order to achieve the required behavior?

    • S.D.
      S.D. over 8 years
      Look like Activity Transitions to me. Have you tried creating 2 Activities and applying material transitions between them ? And used CoordinatorLayout on the 2nd screen ?
    • android developer
      android developer over 8 years
      @S.D. I'm very sure it's not 2 activities, because you can keep touching the screen to scroll and switch between the phases. It doesn't stop you to go to the next/previous activity. When opening a new activity, I don't think it's possible to persist the same touch events for the scrolling mechanism. I'm not even sure if it's possible using fragments, but this is probably more possible than activities.
    • S.D.
      S.D. over 8 years
      Then I think all views are in same layout, with each having a specific Behaviour set on it. And all behaviours are being triggered from vertical scroll interception on root layout which co-ordinates everything.
    • android developer
      android developer over 8 years
      @S.D. Do you know how to make it work well? Is it better than what I've found?
    • Savelii Zagurskii
      Savelii Zagurskii over 8 years
      I think you should take a look at this library.
    • android developer
      android developer over 8 years
      @SaveliiZagurskii Did you try adding what I wrote into the sample of this library?
  • Hardy
    Hardy over 8 years
    but what about this image? cloud.githubusercontent.com/assets/5357526/11641271/… i want to implement this kind of image slide along with bottomsheet goes up
  • android developer
    android developer over 8 years
    @HBdroid I think it's possible. maybe for the "onOffsetChanged" function, also change the translation of the mBottomSheetBackgroundImageView ? The requirement on my case were first to handle the 3 phases. Now it's a question of what to transition and how, and this is very customized and depends on your needs. It also requires a lot of annoying math. I suggest using instant-run feature to make it fast to try.
  • Hardy
    Hardy over 8 years
    i am not able to find the solution please help me
  • android developer
    android developer over 8 years
    @HBdroid Sorry I don't know. Try maybe removing this app:layout_collapseMode="parallax" . also try to do the translation in transformView .
  • N Jay
    N Jay about 8 years
    @Hardy have you ended up creating the solution you wanted ? if yes is it open source and can it be shared ?
  • android developer
    android developer about 8 years
    @NJay I know my answer is quite long, but the github repo is there... :) Here : github.com/AndroidDeveloperLB/ThreePhasesBottomSheet
  • N Jay
    N Jay about 8 years
    Thanks already got your code. cools really cool and appreciate you open sourcing it. Its a big buggy though. if you go from state 2 state changes it will not show the toolbar of the expanded view for example. I was just wondering if there was a ready stable solution before i end up using yours. @androiddeveloper
  • android developer
    android developer about 8 years
    @NJay Yes I know. Sorry for that. If you know how to fix it, please let me know there.
  • android developer
    android developer about 8 years
    The question was made before Google shown their support library class. If you have a working solution using it, please show it here.
  • Vaibhav Sharma
    Vaibhav Sharma about 8 years
    @androiddeveloper I didn't read the date of question and hence I suggested this answer. But if you want to use this library you can use that. As far as the working solution for this code, I don't have it. Sorry.
  • android developer
    android developer almost 8 years
    I've tested the github repo now, and it seems nice. But the upper blue area looks partial sometimes. Also, I can't find out where to handle views that need to move as you drag the bottom sheet. In the repo I've made (here: github.com/AndroidDeveloperLB/ThreePhasesBottomSheet) , the image fades, and the small image moves from one place to another, including changing its size. I'd like to know where to add the handling of those.
  • MiguelHincapieC
    MiguelHincapieC almost 8 years
    Hi there, I have a local version with parallax image but it is not working well yet (I can push it if u wanna look it). You can add any view inside the CoordinatorLayout in activity_main.xml. I guess you have good experience with coordinator layout, otherwise take a look at this link that I found
  • MiguelHincapieC
    MiguelHincapieC almost 8 years
    I'm going to see how did you got toolbar behavior and will use it on mine :D. By the way there is a little behavior that I could only mimic if I use support library 23.2: in google maps if you drag the image that below the toolbar it will move the bottomsheet, but if you upgrade to 23.4 or minSdkVersion 14+ you will lose this behavior o_O'
  • MiguelHincapieC
    MiguelHincapieC almost 8 years
    @androiddeveloper 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
  • Vijay Rajput
    Vijay Rajput over 6 years
    @MiguelHincapieC Hi, I want to only display main toolbar and i removed merged appbar layout but on bottom sheet expand time status bar not displaying and parallex image end to status bar position and i want to parallex image sticky below main toolbar. can you explain how can i do it