In this class we will look at yet another BottomSheetBehavior implementation.
We learn from and re-create an open source example of BottomSheetBehavior mimicking Google Map Behavior created by @miguelhincapie.
It makes use of a library called CustomBottomSheetBehavior and is maintained by @miguelhincapie.
Demo
Here’s the project demo.
1. Installation
We start by installing the library as it is third party. We can easily do this via android studio through the gradle build system.
So proceed over to app level build.gradle and add the following under the dependencies closure:
implementation 'com.mahc.custombottomsheetbehavior:googlemaps-like:0.9.1'
Sync your project after that.
2. Create User Interface
Well we will have three layouts.
(a). activity_main.xml
In our activity_main.xml
we will have a CoordinatorLayout.
Inside it we include a FrameLayout, a placeholder that can show our dummy map.
Our ToolBar will be placed in our AppBarLayout.
Then followed by a ViewPager for our pages.
We will then have our NestedScrollView, with our BottomSheet content placed inside it.
We then have a FloatingActionButton and lastly a custombottomsheetbehavior
.
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout android_id="@+id/coordinatorlayout" android_layout_width="match_parent" android_layout_height="match_parent" android_fitsSystemWindows="true" tools_context="com.mahc.custombottomsheet.MainActivity"> <FrameLayout android_id="@+id/dummy_framelayout_replacing_map" android_layout_width="match_parent" android_layout_height="match_parent" android_background="@android:color/darker_gray" android_fitsSystemWindows="true"/> <!--</FrameLayout>--> <!--<fragment--> <!--android:layout_width="match_parent"--> <!--android:layout_height="match_parent"--> <!--android:id="@+id/support_map"--> <!--android:name="com.google.android.gms.maps.SupportMapFragment"/>--> <android.support.design.widget.AppBarLayout android_id="@+id/appbarlayout" android_layout_width="match_parent" android_layout_height="wrap_content" android_theme="@style/AppTheme.AppBarOverlay" app_layout_behavior="@string/ScrollingAppBarLayoutBehavior"> <android.support.v7.widget.Toolbar android_id="@+id/toolbar" android_layout_width="match_parent" android_layout_height="?attr/actionBarSize" app_popupTheme="@style/AppTheme.PopupOverlay"/> </android.support.design.widget.AppBarLayout> <android.support.v4.view.ViewPager android_id="@+id/pager" android_layout_width="match_parent" android_layout_height="@dimen/anchor_point" android_background="@color/colorAccent" app_layout_behavior="@string/BackDropBottomSheetBehavior" android_fitsSystemWindows="true"> </android.support.v4.view.ViewPager> <android.support.v4.widget.NestedScrollView android_layout_width="match_parent" android_layout_height="match_parent" android_orientation="vertical" app_behavior_peekHeight="@dimen/bottom_sheet_peek_height" android_id="@+id/bottom_sheet" app_layout_behavior="@string/BottomSheetBehaviorGoogleMapsLike" app_anchorPoint="@dimen/anchor_point" app_behavior_hideable="true" android_fitsSystemWindows="true"> <include layout="@layout/bottom_sheet_content" android_layout_width="match_parent" android_layout_height="match_parent" android_fitsSystemWindows="true"/> </android.support.v4.widget.NestedScrollView> <android.support.design.widget.FloatingActionButton android_layout_height="wrap_content" android_layout_width="wrap_content" app_layout_anchor="@id/bottom_sheet" app_layout_anchorGravity="top|right|end" android_src="@drawable/ic_action_go" android_layout_margin="@dimen/fab_margin" app_layout_behavior="@string/ScrollAwareFABBehavior" android_clickable="true" android_focusable="true"/> <com.mahc.custombottomsheetbehavior.MergedAppBarLayout android_id="@+id/mergedappbarlayout" android_layout_width="match_parent" android_layout_height="wrap_content" app_layout_behavior="@string/MergedAppBarLayoutBehavior"/> </android.support.design.widget.CoordinatorLayout>
(b). bottom_sheet_content.xml
Next we have the bottom_sheet_content
layout. At the root we have a LinearLayout.
This layout will be displaying our content which will be rendered in TextViews and ImageViews,so we add those widgets as well as ImageButton.
Here are the xml widgets we make use of in this layout.
as well as Viewgroups like:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android_layout_width="match_parent" android_layout_height="match_parent" android_orientation="vertical" android_paddingBottom="16dp" android_background="@android:color/white"> <RelativeLayout android_layout_width="match_parent" android_layout_height="@dimen/bottom_sheet_peek_height" android_background="@color/colorPrimary" android_paddingTop="8dp" android_paddingStart="16dp" android_paddingEnd="16dp" android_paddingBottom="8dp"> <LinearLayout android_layout_width="wrap_content" android_layout_height="match_parent" android_orientation="vertical"> <TextView android_id="@+id/bottom_sheet_title" android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="Title dummy" android_textColor="@android:color/white" android_textSize="19sp"/> <TextView android_id="@+id/text_dummy1" android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="Text dummy 1234 1234" android_textColor="@android:color/white" android_textSize="12sp" android_layout_marginTop="5dp"/> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="Text dummy qwer asdf zxcv" android_textColor="@android:color/white" android_textSize="12sp" android_layout_marginTop="5dp"/> </LinearLayout> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="5 min" android_textColor="@android:color/white" android_textSize="14sp" android_layout_alignParentEnd="true" android_layout_centerVertical="true" android_paddingTop="8dp"/> </RelativeLayout> <LinearLayout android_layout_width="match_parent" android_layout_height="wrap_content" android_orientation="horizontal" android_gravity="center_horizontal" android_paddingStart="16dp" android_paddingEnd="16dp" android_paddingBottom="12dp" android_background="@drawable/border_bottom"> <RelativeLayout android_layout_width="wrap_content" android_layout_height="match_parent"> <ImageButton android_id="@+id/button1" android_layout_width="70dp" android_layout_height="60dp" android_src="@android:drawable/ic_dialog_map" android_layout_centerHorizontal="true" android_background="?attr/selectableItemBackgroundBorderless"/> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="BUTTON 1" android_textColor="@android:color/black" android_layout_alignParentBottom="true"/> </RelativeLayout> <LinearLayout android_id="@+id/establecimiento_layout_favoritos_button" android_layout_width="wrap_content" android_layout_height="wrap_content" android_orientation="vertical" android_layout_marginEnd="20dp" android_layout_marginStart="20dp"> <ImageButton android_id="@+id/button2" android_layout_width="70dp" android_layout_height="60dp" android_src="@android:drawable/ic_dialog_info" android_layout_centerHorizontal="true" android_background="?attr/selectableItemBackgroundBorderless"/> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="BUTTON 2" android_textColor="@android:color/black"/> </LinearLayout> <RelativeLayout android_layout_width="wrap_content" android_layout_height="match_parent"> <ImageButton android_id="@+id/establecimiento_share_button" android_layout_width="70dp" android_layout_height="60dp" android_src="@android:drawable/ic_menu_share" android_layout_centerHorizontal="true" android_background="?attr/selectableItemBackgroundBorderless"/> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="BUTTON 3" android_textColor="@android:color/black" android_layout_alignParentBottom="true"/> </RelativeLayout> </LinearLayout> <RelativeLayout android_layout_width="match_parent" android_layout_height="wrap_content" android_background="@drawable/border_bottom"> <ImageView android_id="@+id/establecimiento_icon_sucursales" android_layout_width="wrap_content" android_layout_height="wrap_content" app_srcCompat="@android:drawable/ic_secure" android_layout_marginEnd="16dp" android_layout_marginTop="12dp" android_layout_marginStart="16dp" android_layout_alignParentStart="true" android_layout_alignParentTop="true"/> <LinearLayout android_layout_width="match_parent" android_layout_height="wrap_content" android_orientation="vertical" android_layout_toEndOf="@id/establecimiento_icon_sucursales" android_layout_marginStart="16dp" android_layout_marginEnd="16dp"> <FrameLayout android_layout_width="match_parent" android_layout_height="48dp"> <Button android_id="@+id/establecimiento_sucursal_row_button" android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="Text dummy 1" android_layout_gravity="center_vertical" android_background="?attr/selectableItemBackgroundBorderless" android_textAllCaps="false" android_textColor="@android:color/black"/> </FrameLayout> <FrameLayout android_layout_width="match_parent" android_layout_height="48dp"> <Button android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="Text dummy 2" android_layout_gravity="center_vertical" android_background="?attr/selectableItemBackgroundBorderless" android_textAllCaps="false" android_textColor="@android:color/black"/> </FrameLayout> <FrameLayout android_layout_width="match_parent" android_layout_height="48dp"> <Button android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="Text dummy 3" android_layout_gravity="center_vertical" android_background="?attr/selectableItemBackgroundBorderless" android_textAllCaps="false" android_textColor="@android:color/black"/> </FrameLayout> </LinearLayout> </RelativeLayout> <FrameLayout android_layout_width="match_parent" android_layout_height="56dp" android_layout_marginStart="16dp" android_layout_marginEnd="16dp"> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_text="APACHE LICENSE" android_layout_gravity="center_vertical" android_textSize="18sp" android_textColor="@android:color/darker_gray"/> </FrameLayout> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_layout_margin="16dp" android_text="@string/dummy_text" android_textSize="12sp" android_textColor="@android:color/black"/> <FrameLayout android_layout_width="match_parent" android_layout_height="620dp" android_background="@color/colorAccent" android_layout_marginTop="40dp"> <TextView android_layout_width="wrap_content" android_layout_height="wrap_content" android_layout_gravity="center" android_text="Your remaining content here" android_textColor="@android:color/white" /> </FrameLayout> </LinearLayout>
(c). pager_item.xml
Then the pager_item.xml
.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android_orientation="vertical" android_layout_width="match_parent" android_layout_height="match_parent"> <ImageView android_layout_width="match_parent" android_layout_height="match_parent" android_id="@+id/imageView" android_scaleType="centerCrop"/> </LinearLayout>
Our Java Code
(a). ItemPagerAdapter.java
First create a new class. Call it a ItemPagerAdapter
and let’s make it derive from android.support.v4.view.PagerAdapter
.
public class ItemPagerAdapter extends android.support.v4.view.PagerAdapter {..}
PagerAdapter acts as the base class for providing the adapter to populate pages inside of that ViewPager.
Add three instance fields in our class:
Context mContext; LayoutInflater mLayoutInflater; final int[] mItems;
First the Context is an interface to global information about an application environment. Normally Context allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
In this case it will allow us inflate our layout into a view.
Then we have a LayoutInflater, a class that allows us instantiates a layout XML file into its corresponding View objects.
The int array will hold us image ids.
Then our constructor is responsible for first receiving a context object which will be required during inflation of our pager_item
layout and secondly the int
items which will hold our image ids.
We will also perform the inflation inside that constructor.
public ItemPagerAdapter(Context context, int[] items) { this.mContext = context; this.mLayoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.mItems = items; }
We then override the other methods.
Here’s the full code for this class:
package com.mahc.custombottomsheet; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; import com.mahc.custombottomsheet.R; public class ItemPagerAdapter extends android.support.v4.view.PagerAdapter { Context mContext; LayoutInflater mLayoutInflater; final int[] mItems; public ItemPagerAdapter(Context context, int[] items) { this.mContext = context; this.mLayoutInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); this.mItems = items; } @Override public int getCount() { return mItems.length; } @Override public boolean isViewFromObject(View view, Object object) { return view == ((LinearLayout) object); } @Override public Object instantiateItem(ViewGroup container, int position) { View itemView = mLayoutInflater.inflate(R.layout.pager_item, container, false); ImageView imageView = (ImageView) itemView.findViewById(R.id.imageView); imageView.setImageResource(mItems[position]); container.addView(itemView); return itemView; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((LinearLayout) object); } }
(b). MainActivity.java
Finally here’s our main activity. Maybe it’s been generate to you by android studio.
So go ahead and as instance fields add an int array to hold our Drawables.
Also we’ll have a TextView:
int[] mDrawables = { R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3 }; TextView bottomSheetTextView;
We start by referencing our widgets CoordinatorLayout as well as View to represent the bottom sheet.
CoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.coordinatorlayout); View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet);
Then we reference the BottomSheetBehaviorGoogleMapsLike
. This is the library we are using to create a bottom sheet behavior like that in Google maps.
final BottomSheetBehaviorGoogleMapsLike behavior = BottomSheetBehaviorGoogleMapsLike.from(bottomSheet);
Then listen to various callbacks:
behavior.addBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { } } @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { } });
We are interested especially in the state change event where we get various state of our BottomSheetBehaviorGoogleMapsLike.
Here’s the code:
package com.mahc.custombottomsheet; import android.support.annotation.NonNull; import android.support.design.widget.AppBarLayout; import android.support.design.widget.CoordinatorLayout; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.View; import android.widget.TextView; import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike; import com.mahc.custombottomsheetbehavior.MergedAppBarLayout; import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior; public class MainActivity extends AppCompatActivity { int[] mDrawables = { R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3, R.drawable.cheese_3 }; TextView bottomSheetTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setTitle(" "); } /** * If we want to listen for states callback */ CoordinatorLayout coordinatorLayout = (CoordinatorLayout) findViewById(R.id.coordinatorlayout); View bottomSheet = coordinatorLayout.findViewById(R.id.bottom_sheet); final BottomSheetBehaviorGoogleMapsLike behavior = BottomSheetBehaviorGoogleMapsLike.from(bottomSheet); behavior.addBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { switch (newState) { case BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED: Log.d("bottomsheet-", "STATE_COLLAPSED"); break; case BottomSheetBehaviorGoogleMapsLike.STATE_DRAGGING: Log.d("bottomsheet-", "STATE_DRAGGING"); break; case BottomSheetBehaviorGoogleMapsLike.STATE_EXPANDED: Log.d("bottomsheet-", "STATE_EXPANDED"); break; case BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT: Log.d("bottomsheet-", "STATE_ANCHOR_POINT"); break; case BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN: Log.d("bottomsheet-", "STATE_HIDDEN"); break; default: Log.d("bottomsheet-", "STATE_SETTLING"); break; } } @Override public void onSlide(@NonNull View bottomSheet, float slideOffset) { } }); MergedAppBarLayout mergedAppBarLayout = findViewById(R.id.mergedappbarlayout); MergedAppBarLayoutBehavior mergedAppBarLayoutBehavior = MergedAppBarLayoutBehavior.from(mergedAppBarLayout); mergedAppBarLayoutBehavior.setToolbarTitle("Title Dummy"); mergedAppBarLayoutBehavior.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { behavior.setState(BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT); } }); bottomSheetTextView = (TextView) bottomSheet.findViewById(R.id.bottom_sheet_title); ItemPagerAdapter adapter = new ItemPagerAdapter(this,mDrawables); ViewPager viewPager = (ViewPager) findViewById(R.id.pager); viewPager.setAdapter(adapter); behavior.setState(BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT); //behavior.setCollapsible(false); } }
(d). How to Run.
The download contains both a library as well as code. Just go to the app folder and you find the code for the sample.
Go over to your app level build.gradle and add statement via the implementation statement just as we did in the Installation section.
No. | Resource | Action |
---|---|---|
1. | GitHub | Download |
2. | GitHub | Browse |
3. | Project Thanks to | @miguelhincapie |