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

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


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.


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:

    @Overridepublic 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


I also had to implement a view similar to how Google Maps shows a bottom-sheet for a found result.

Here's how mine looks:

Peek view

Expand view scrolled to top

Expand view scrolled to bottom

At first, I defined a bottom sheet with a header and scrollable content but the layout_height did not seem to wrap the content neither of the header nor the scrollable content despite specifying wrap_content.

That problem went away when I used LinearLayout instead of ConstraintLayout for the CoordinatorLayout's child layout (and for its children).

activity_main.xml

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.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="match_parent"    tools:context=".MainActivity">    <Button        android:id="@+id/buttonPeek"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="Peek"        app:layout_constraintEnd_toStartOf="@+id/buttonExpand"        app:layout_constraintHorizontal_bias="0.5"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent" />    <Button        android:id="@+id/buttonExpand"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="Expand"        app:layout_constraintEnd_toStartOf="@+id/buttonClose"        app:layout_constraintHorizontal_bias="0.5"        app:layout_constraintStart_toEndOf="@+id/buttonPeek"        app:layout_constraintTop_toTopOf="@+id/buttonPeek" />    <Button        android:id="@+id/buttonClose"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="Close"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintHorizontal_bias="0.5"        app:layout_constraintStart_toEndOf="@+id/buttonExpand"        app:layout_constraintTop_toTopOf="@+id/buttonExpand" />    <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:id="@+id/layout_coordinator"        android:layout_width="match_parent"        android:layout_height="wrap_content"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent">        <LinearLayout            android:id="@+id/layout_coordinator_child"            android:layout_width="match_parent"            android:layout_height="wrap_content"            android:orientation="vertical"            app:behavior_hideable="true"            app:layout_behavior="@string/bottom_sheet_behavior">            <LinearLayout                android:id="@+id/layout_bottom_sheet_header"                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:background="#FFFF0000"                android:orientation="vertical" >                <TextView                    android:id="@+id/headerTextView_a"                    android:layout_width="wrap_content"                    android:layout_height="wrap_content"                    android:text="a" />                <TextView                android:id="@+id/headerTextView_b"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="b" />                <TextView                android:id="@+id/headerTextView_c"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="c" />                <TextView                android:id="@+id/headerTextView_d"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="d" />                <TextView                android:id="@+id/headerTextView_e"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="e" />                <TextView                android:id="@+id/headerTextView_f"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="f" />                <TextView                android:id="@+id/headerTextView_g"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="g" />                <TextView                android:id="@+id/headerTextView_h"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="h" />                <TextView                android:id="@+id/headerTextView_i"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="i" />                <TextView                android:id="@+id/headerTextView_j"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="j" />                <TextView                android:id="@+id/headerTextView_k"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:text="k" />            </LinearLayout>            <androidx.core.widget.NestedScrollView                android:id="@+id/layout_bottom_sheet_scrollable_view"                android:layout_width="match_parent"                android:layout_height="wrap_content"                android:background="#FF00FF00"                android:fillViewport="true" >                <LinearLayout                    android:id="@+id/layout_bottom_sheet_scrollable_content"                    android:layout_width="match_parent"                    android:layout_height="wrap_content"                    android:orientation="vertical">                    <TextView                        android:id="@+id/textView0"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="0" />                    <TextView                        android:id="@+id/textView1"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="1" />                    <TextView                        android:id="@+id/textView2"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="2" />                    <TextView                        android:id="@+id/textView3"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="3" />                    <TextView                        android:id="@+id/textView4"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="4" />                    <TextView                        android:id="@+id/textView5"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="5" />                    <TextView                        android:id="@+id/textView6"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="6" />                    <TextView                        android:id="@+id/textView7"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="7" />                    <TextView                        android:id="@+id/textView8"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="8" />                    <TextView                        android:id="@+id/textView9"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="9" />                    <TextView                        android:id="@+id/textView10"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="10" />                    <TextView                        android:id="@+id/textView11"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="11" />                    <TextView                        android:id="@+id/textView12"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="12" />                    <TextView                        android:id="@+id/textView13"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="13" />                    <TextView                        android:id="@+id/textView14"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="14" />                    <TextView                        android:id="@+id/textView15"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="15" />                    <TextView                        android:id="@+id/textView16"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="16" />                    <TextView                        android:id="@+id/textView17"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="17" />                    <TextView                        android:id="@+id/textView18"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="18" />                    <TextView                        android:id="@+id/textView19"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="19" />                    <TextView                        android:id="@+id/textView20"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="20" />                    <TextView                        android:id="@+id/textView21"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="21" />                    <TextView                        android:id="@+id/textView22"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="22" />                    <TextView                        android:id="@+id/textView23"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="23" />                    <TextView                        android:id="@+id/textView24"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="24" />                    <TextView                        android:id="@+id/textView25"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="25" />                    <TextView                        android:id="@+id/textView26"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="26" />                    <TextView                        android:id="@+id/textView27"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="27" />                    <TextView                        android:id="@+id/textView28"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="28" />                    <TextView                        android:id="@+id/textView29"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="29" />                    <TextView                        android:id="@+id/textView30"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="30" />                    <TextView                        android:id="@+id/textView31"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="31" />                    <TextView                        android:id="@+id/textView32"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="32" />                    <TextView                        android:id="@+id/textView33"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="33" />                    <TextView                        android:id="@+id/textView34"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="34" />                    <TextView                        android:id="@+id/textView35"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="35" />                    <TextView                        android:id="@+id/textView36"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="36" />                    <TextView                        android:id="@+id/textView37"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="37" />                    <TextView                        android:id="@+id/textView38"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="38" />                    <TextView                        android:id="@+id/textView39"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="39" />                    <TextView                        android:id="@+id/textView40"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="40" />                    <TextView                        android:id="@+id/textView41"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="41" />                    <TextView                        android:id="@+id/textView42"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="42" />                    <TextView                        android:id="@+id/textView43"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="43" />                    <TextView                        android:id="@+id/textView44"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="44" />                    <TextView                        android:id="@+id/textView45"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="45" />                    <TextView                        android:id="@+id/textView46"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="46" />                    <TextView                        android:id="@+id/textView47"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="47" />                    <TextView                        android:id="@+id/textView48"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="48" />                    <TextView                        android:id="@+id/textView49"                        android:layout_width="wrap_content"                        android:layout_height="wrap_content"                        android:text="49" />                </LinearLayout>            </androidx.core.widget.NestedScrollView>        </LinearLayout>    </androidx.coordinatorlayout.widget.CoordinatorLayout></androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java

package com.example.bottomsheetwithscrollablecontent;import android.os.Bundle;import android.view.View;import android.widget.Button;import com.google.android.material.bottomsheet.BottomSheetBehavior;import androidx.appcompat.app.AppCompatActivity;import androidx.coordinatorlayout.widget.CoordinatorLayout;public class MainActivity extends AppCompatActivity {    private CoordinatorLayout layout_coordinator;    private View layout_coordinator_child;    private View layout_bottom_sheet_header;    private BottomSheetBehavior behavior;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        layout_coordinator = findViewById(R.id.layout_coordinator);        layout_coordinator_child = layout_coordinator.findViewById(R.id.layout_coordinator_child);        layout_bottom_sheet_header = layout_coordinator.findViewById(R.id.layout_bottom_sheet_header);        behavior = BottomSheetBehavior.from(layout_coordinator_child);        Button buttonPeek = findViewById(R.id.buttonPeek);        buttonPeek.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                behavior.setPeekHeight(layout_bottom_sheet_header.getHeight());                behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);            }        });        Button buttonExpand = findViewById(R.id.buttonExpand);        buttonExpand.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                behavior.setState(BottomSheetBehavior.STATE_EXPANDED);            }        });        Button buttonClose = findViewById(R.id.buttonClose);        buttonClose.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                behavior.setState(BottomSheetBehavior.STATE_HIDDEN);            }        });    }}

app/build.gradle

apply plugin: 'com.android.application'android {    compileSdkVersion 28    defaultConfig {        applicationId "com.example.bottomsheetwithscrollablecontent"        minSdkVersion 24        targetSdkVersion 28        versionCode 1        versionName "1.0"        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"    }    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'        }    }}dependencies {    implementation fileTree(dir: 'libs', include: ['*.jar'])    implementation 'androidx.appcompat:appcompat:1.0.0-beta01'    implementation 'androidx.constraintlayout:constraintlayout:1.1.2'    testImplementation 'junit:junit:4.12'    androidTestImplementation 'androidx.test:runner:1.1.0-alpha4'    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha4'    implementation 'androidx.legacy:legacy-support-v4:1.0.0-beta01'    implementation "com.google.android.material:material:1.1.0-alpha04"}