Friday, May 6, 2016

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

Leave a Comment

Background

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

It has 3 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 need handling of the toolbar on multiple phases.

What I've tried

I've found a good (enough) library for 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?

2 Answers

Answers 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)); 

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

Hope it doesn't have any bugs.


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

  1. only works well on Android 6 and above. other 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 be peeked.

If anyone can help with it, please do.


EDIT: 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.


EDIT: 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.


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

Answers 2

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.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment