I\'m assigned to make a UI that behaves similar to how Google Maps shows a bottom-sheet for a found result.
It has three different phases:
BIG UPDATE
Because there were like 4 or 5 question 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 modifying the default BottomSheetBehavior
, adding one more stat with following steps:
CoordinatorLayout.Behavior<V>
BottomSheetBehavior
file to your new one.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);
}
Add a new state:
public static final int STATE_ANCHOR_POINT = X;
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:
Did you try this? http://android-developers.blogspot.in/2016/02/android-support-library-232.html?m=1 In here it says we can just specify a bottom sheet layout behaviour.
UPDATE:
Basically the link states-
By attaching a BottomSheetBehavior to a child View of a CoordinatorLayout (i.e., adding app:layout_behavior="android.support.design.widget.BottomSheetBehavior"), you’ll automatically get the appropriate touch detection to transition between five state:
STATE_COLLAPSED: this collapsed state is the default and shows just a portion of the layout along the bottom. The height can be controlled with the app:behavior_peekHeight attribute (defaults to 0)
STATE_DRAGGING: the intermediate state while the user is directly dragging the bottom sheet up or down
STATE_SETTLING: that brief time between when the View is released and settling into its final position
STATE_EXPANDED: the fully expanded state of the bottom sheet, where either the whole bottom sheet is visible (if its height is less than the containing CoordinatorLayout) or the entire CoordinatorLayout is filled
STATE_HIDDEN: disabled by default (and enabled with the app:behavior_hideable attribute), enabling this allows users to swipe down on the bottom sheet to completely hide the bottom sheet
Keep in mind that scrolling containers in your bottom sheet must support nested scrolling (for example, NestedScrollView, RecyclerView, or ListView/ScrollView on API 21+).
If you’d like to receive callbacks of state changes, you can add a BottomSheetCallback:
// The View with the BottomSheetBehavior
View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet);
behavior.setBottomSheetCallback(new BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
// React to state change
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
// React to dragging events
}
});
While BottomSheetBehavior captures the persistent bottom sheet case, this release also provides a BottomSheetDialog and BottomSheetDialogFragment to fill the modal bottom sheets use case. Simply replace AppCompatDialog or AppCompatDialogFragment with their bottom sheet equivalents to have your dialog styled as a bottom sheet.
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:
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);
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:
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 isn't peeked 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.
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:
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).
<?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>
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);
}
});
}
}
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"
}