Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sliding up image with Official Support Library 23.x.+ bottomSheet like google maps

Update
I want to accomplish the same behavior that google maps have with Support Library 23.x.+ and without ANY 3rd library

NOTE: this is not a duplicated question because:

  1. I want to use Behaviors, Support Library and without ANY 3rd party library (I added it in question title and above description)
  2. I wanted ALL behaviors that you see in the next gif, the other questions are asking for one or two behaviors and using anyway to achieve it.

    like you can see in this gif

I have already the Official bottomSheet working (even inside a tab and view pager).

What is making me going crazy is how to achieve the image behavior that comes up from the BottomSheet when sliding up using the official bottomSheet?.

I have tried using anchor like FAB with no success.
I read something about using a scroll listener but ppl said it's not smooth and faster like google maps.

My XML (I don't think it's going to help but anyway):

<?xml version="1.0" encoding="utf-8"?> <android.support.design.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:layout_width="match_parent"     android:layout_height="match_parent"     tools:context=".ui.MasterActivity">      <android.support.design.widget.AppBarLayout         android:layout_width="match_parent"         android:layout_height="wrap_content"         android:theme="@style/AppTheme.AppBarOverlay">          <android.support.v7.widget.Toolbar             android:id="@+id/toolbar"             android:layout_width="match_parent"             android:layout_height="?attr/actionBarSize"             android:background="?attr/colorPrimary"             app:popupTheme="@style/AppTheme.PopupOverlay"             app:layout_scrollFlags="scroll|enterAlways|snap">              <Button                 android:layout_width="wrap_content"                 android:layout_height="wrap_content"                 style="?android:attr/borderlessButtonStyle"                 android:text="Departure"                 android:layout_gravity="center"                 android:id="@+id/buttonToolBar"                 />           </android.support.v7.widget.Toolbar>          <android.support.design.widget.TabLayout             android:id="@+id/tabs"             android:layout_width="match_parent"             android:layout_height="wrap_content"             app:tabBackground="@android:color/white"             app:tabTextColor="@color/colorAccent"             app:tabSelectedTextColor="@color/colorAccent"/>      </android.support.design.widget.AppBarLayout>      <android.support.v4.view.ViewPager         android:id="@+id/viewpager"         android:layout_width="match_parent"         android:layout_height="match_parent"         app:layout_behavior="@string/appbar_scrolling_view_behavior" />               <android.support.v4.widget.NestedScrollView         android:id="@+id/asdf"         android:layout_width="match_parent"         android:layout_height="match_parent"         android:orientation="vertical"         app:behavior_peekHeight="100dp"         android:fitsSystemWindows="true"             app:layout_behavior="android.support.design.widget.BottomSheetBehavior">          <LinearLayout             android:id="@+id/qwert"             android:layout_width="match_parent"             android:layout_height="match_parent"             android:orientation="vertical"             android:paddingBottom="16dp"             android:background="@android:color/white"             android:padding="15dp">              <TextView                 android:layout_width="match_parent"                 android:layout_height="wrap_content"                 android:text="BOOTOMSHEET TITLE"                     android:textAppearance="@style/TextAppearance.AppCompat.Title" />              <Button                 android:layout_width="match_parent"                 android:layout_height="wrap_content"                 android:text="Button1"/>              <TextView                 android:layout_width="match_parent"                 android:layout_height="wrap_content"                 android:text="text 2"                 android:layout_margin="10dp"/>              <TextView                 android:layout_width="match_parent"                 android:layout_height="wrap_content"                 android:text="text 3"                 android:layout_margin="10dp"/>              <TextView                 android:layout_width="match_parent"                 android:layout_height="wrap_content"                 android:text="text 4"                 android:layout_margin="10dp"/>               <FrameLayout                 android:layout_width="match_parent"                 android:layout_height="320dp"                 android:background="@color/colorAccent">                  <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>     </android.support.v4.widget.NestedScrollView>       <android.support.design.widget.FloatingActionButton         android:layout_height="wrap_content"         android:layout_width="wrap_content"         app:layout_anchor="@id/asdf"         app:layout_anchorGravity="top|right|end"         android:src="@drawable/abc_ic_search_api_mtrl_alpha_copy"         android:layout_margin="@dimen/fab_margin"         android:clickable="true"/>  </android.support.design.widget.CoordinatorLayout> 
like image 872
MiguelHincapieC Avatar asked May 19 '16 22:05

MiguelHincapieC


1 Answers

If you want to achieve it using Support Library 23.4.0.+ I will tell you how I got it and how its works.

As far I can see that activity/fragment has the followings behaviors:

  1. 2 toolbars with animations that respond to the bottom sheet movements.
  2. A FAB that hides when it is near to the "modal toolbar" (the one that appears when you are sliding up).
  3. A backdrop image behind the bottom sheet with some kind of parallax effect.
  4. A Title (TextView) in Toolbar that appears when the bottom sheet reaches it.
  5. The notification status bar can turn its background to transparent or full color.
  6. A custom bottom sheet behavior with an "anchor" state.

note2: This answer talk about 6 things not about 1 or 2 like other question, can you see the difference now?

Ok, now let's check one bye one:

ToolBars
When you open that view in google maps u can see a toolbar where you can search, it's the only one that I'm not doing equals like google maps because I wanted to do it more generic. Anyway, that ToolBar is inside an AppBarLayout and it got hidden when you start dragging the BottomSheet and it appears again when the BottomSheet reaches the COLLAPSED state.
To achieve it you need:

  • create a Behavior and extend it from AppBarLayout.ScrollingViewBehavior
  • override layoutDependsOn and onDependentViewChanged methods. Doing it you will listen for bottomSheet movements.
  • create some methods to hide and unhide the AppBarLayout/ToolBar with animations.

This is how I did it for the first toolbar or ActionBar:

@Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {     return dependency instanceof NestedScrollView; }  @Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child,                                       View dependency) {      if (mChild == null) {         initValues(child, dependency);         return false;     }      float dVerticalScroll = dependency.getY() - mPreviousY;     mPreviousY = dependency.getY();      //going up     if (dVerticalScroll <= 0 && !hidden) {         dismissAppBar(child);         return true;     }      return false; }  private void initValues(final View child, View dependency) {      mChild = child;     mInitialY = child.getY();      BottomSheetBehaviorGoogleMapsLike bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(dependency);     bottomSheetBehavior.addBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {         @Override         public void onStateChanged(@NonNull View bottomSheet, @BottomSheetBehaviorGoogleMapsLike.State int newState) {             if (newState == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED ||                     newState == BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN)                 showAppBar(child);         }          @Override         public void onSlide(@NonNull View bottomSheet, float slideOffset) {          }     }); }  private void dismissAppBar(View child){     hidden = true;     AppBarLayout appBarLayout = (AppBarLayout)child;     mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_shortAnimTime));     mToolbarAnimation.y(-(mChild.getHeight()+25)).start(); }  private void showAppBar(View child) {     hidden = false;     AppBarLayout appBarLayout = (AppBarLayout)child;     mToolbarAnimation = appBarLayout.animate().setDuration(mContext.getResources().getInteger(android.R.integer.config_mediumAnimTime));     mToolbarAnimation.y(mInitialY).start(); } 

the complete file if you need it

The second Toolbar or "Modal" toolbar:
You have to override some methods but in this one, you have to take care of more behaviors:

  • show/hide the ToolBar with animations
  • change status bar color/background
  • show/hide the BottomSheet title in the ToolBar
  • close the bottomSheet or send it to a collapsed state

The code for this one is a little extensive so I will let the link

The FAB

This is a Custom Behavior too but extends from FloatingActionButton.Behavior. In onDependentViewChanged you have to look when it reaches the "offSet" or point in where you want to hide it. In my case I want to hide it when it's near to the second toolbar, so I dig into FAB parent (a CoordiantorLayout) looking for the AppBarLayout that contains the ToolBar, then I use the ToolBar position like OffSet:

@Override public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) {      if (offset == 0)         setOffsetValue(parent);      if (dependency.getY() <=0)         return false;      if (child.getY() <= (offset + child.getHeight()) && child.getVisibility() == View.VISIBLE)         child.hide();     else if (child.getY() > offset && child.getVisibility() != View.VISIBLE)         child.show();      return false; } 

Complete Custom FAB Behavior link

The Image behind the BottomSheet with parallax effect:
Like the others its a custom behavior, the only "complicated" thing in this one is the little algorithm that keeps the Image anchored to the BottomSheet and avoids the image collapse like the default parallax effect:

@Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child,                                       View dependency) {      if (mYmultiplier == 0) {         initValues(child, dependency);         return true;     }      float dVerticalScroll = dependency.getY() - mPreviousY;     mPreviousY = dependency.getY();      //going up     if (dVerticalScroll <= 0 && child.getY() <= 0) {         child.setY(0);         return true;     }      //going down     if (dVerticalScroll >= 0 && dependency.getY() <= mImageHeight)         return false;      child.setY( (int)(child.getY() + (dVerticalScroll * mYmultiplier) ) );      return true; } 

[complete file for backdrop Image with parallax effect][4]

Now for the end: The Custom BottomSheet Behavior
To achieve the 3 steps first you need to understand that default BottomSheetBehavior has 5 states: STATE_DRAGGING, STATE_SETTLING, STATE_EXPANDED, STATE_COLLAPSED, STATE_HIDDEN, and for the Google Maps behavior you need to add a middle state between collapsed and expanded: STATE_ANCHOR_POINT.
I tried extends the default bottomSheetBehavior with no success, so I just copy-paste all code and modified what I need.
To achieve what I'm talking about following the next 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)



public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {     // First let the parent lay it out     if (mState != STATE_DRAGGING && mState != STATE_SETTLING) {         if (ViewCompat.getFitsSystemWindows(parent) &&                 !ViewCompat.getFitsSystemWindows(child)) {             ViewCompat.setFitsSystemWindows(child, true);         }         parent.onLayoutChild(child, layoutDirection);     }     // Offset the bottom sheet     mParentHeight = parent.getHeight();     mMinOffset = Math.max(0, mParentHeight - child.getHeight());     mMaxOffset = Math.max(mParentHeight - mPeekHeight, mMinOffset);      //if (mState == STATE_EXPANDED) {     //    ViewCompat.offsetTopAndBottom(child, mMinOffset);     //} else if (mHideable && mState == STATE_HIDDEN...     if (mState == STATE_ANCHOR_POINT) {         ViewCompat.offsetTopAndBottom(child, mAnchorPoint);     } else if (mState == STATE_EXPANDED) {         ViewCompat.offsetTopAndBottom(child, mMinOffset);     } else if (mHideable && mState == STATE_HIDDEN) {         ViewCompat.offsetTopAndBottom(child, mParentHeight);     } else if (mState == STATE_COLLAPSED) {         ViewCompat.offsetTopAndBottom(child, mMaxOffset);     }     if (mViewDragHelper == null) {         mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);     }     mViewRef = new WeakReference<>(child);     mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));     return true; }   public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {     if (child.getTop() == mMinOffset) {         setStateInternal(STATE_EXPANDED);         return;     }     if (target != mNestedScrollingChildRef.get() || !mNestedScrolled) {         return;     }     int top;     int targetState;     if (mLastNestedScrollDy > 0) {         //top = mMinOffset;         //targetState = STATE_EXPANDED;         int currentTop = child.getTop();         if (currentTop > mAnchorPoint) {             top = mAnchorPoint;             targetState = STATE_ANCHOR_POINT;         }         else {             top = mMinOffset;             targetState = STATE_EXPANDED;         }     } else if (mHideable && shouldHide(child, getYVelocity())) {         top = mParentHeight;         targetState = STATE_HIDDEN;     } else if (mLastNestedScrollDy == 0) {         int currentTop = child.getTop();         if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {             top = mMinOffset;             targetState = STATE_EXPANDED;         } else {             top = mMaxOffset;             targetState = STATE_COLLAPSED;         }     } else {         //top = mMaxOffset;         //targetState = STATE_COLLAPSED;         int currentTop = child.getTop();         if (currentTop > mAnchorPoint) {             top = mMaxOffset;             targetState = STATE_COLLAPSED;         }         else {             top = mAnchorPoint;             targetState = STATE_ANCHOR_POINT;         }     }     if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {         setStateInternal(STATE_SETTLING);         ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));     } else {         setStateInternal(targetState);     }     mNestedScrolled = false; }  public final void setState(@State int state) {     if (state == mState) {         return;     }     if (mViewRef == null) {         // The view is not laid out yet; modify mState and let onLayoutChild handle it later         /**          * New behavior (added: state == STATE_ANCHOR_POINT ||)          */         if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||                 state == STATE_ANCHOR_POINT ||                 (mHideable && state == STATE_HIDDEN)) {             mState = state;         }         return;     }     V child = mViewRef.get();     if (child == null) {         return;     }     int top;     if (state == STATE_COLLAPSED) {         top = mMaxOffset;     } else if (state == STATE_ANCHOR_POINT) {         top = mAnchorPoint;     } else if (state == STATE_EXPANDED) {         top = mMinOffset;     } else if (mHideable && state == STATE_HIDDEN) {         top = mParentHeight;     } else {         throw new IllegalArgumentException("Illegal state argument: " + state);     }     setStateInternal(STATE_SETTLING);     if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {         ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));     } }   public static <V extends View> BottomSheetBehaviorGoogleMapsLike<V> from(V view) {     ViewGroup.LayoutParams params = view.getLayoutParams();     if (!(params instanceof CoordinatorLayout.LayoutParams)) {         throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");     }     CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)             .getBehavior();     if (!(behavior instanceof BottomSheetBehaviorGoogleMapsLike)) {         throw new IllegalArgumentException(                 "The view is not associated with BottomSheetBehaviorGoogleMapsLike");     }     return (BottomSheetBehaviorGoogleMapsLike<V>) behavior; } 



link to the hole project in where you can see all Custom Behaviors

note3: next time add a comment asking in a polite way for a change of the answer or ask why this answer has SOME equals stuff than others answer of mine about the same topic BEFORE close it or mark like duplicated.

And here is how it looks like
[CustomBottomSheetBehavior]

like image 165
MiguelHincapieC Avatar answered Oct 14 '22 03:10

MiguelHincapieC