37

I am trying to have a collapsing toolbar that is similar to the Google Maps app in the search landing page. That is, there are three "anchor points" or positions. In place of the map, I will have a picture.

  • Toolbar collapsed (content is fullscreen)

Fullscreen

  • Middle position

Half way

  • Toolbar extended with only some content showing (persistent bottom sheet)

All the way open

Preferably, the app should snap between these positions.

As of now I have the layout basically working.

Two main issues are:

  • Flinging inside the NestedScrollView does not work correctly. It halts/chops, even though it's using app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior". I believe this is a bug with AppBarLayout
  • The anchor points described above are not implemented.

This is my layout:

Note that app:layout_behavior="@string/appbar_anchor_behavior"> is just an unmodified subclass of AppBarLayout.Behavior

<android.support.design.widget.CoordinatorLayout 
 xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:app="http://schemas.android.com/apk/res-auto"
 android:id="@+id/main_content"                                                         
 android:layout_width="match_parent"                                               
 android:layout_height="match_parent"                                                           
 android:background="@color/actions_bar_dark"                                                 
 android:fitsSystemWindows="true">


<android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:fitsSystemWindows="true"
        app:layout_behavior="@string/appbar_anchor_behavior">

    <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll"
            android:fitsSystemWindows="true">

        <ImageView
                android:id="@+id/item_preview_thumb"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:scaleType="centerCrop"
                android:layout_centerInParent="true"
                app:layout_collapseMode="parallax"
                />

        <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                app:layout_collapseMode="pin" />

    </android.support.design.widget.CollapsingToolbarLayout>

</android.support.design.widget.AppBarLayout>

<android.support.v4.widget.NestedScrollView
        android:id="@+id/contentRecyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">


    <include layout="@layout/item_detail_content"/>


</android.support.v4.widget.NestedScrollView>

<android.support.design.widget.FloatingActionButton
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        app:layout_anchor="@id/appbar"
        app:layout_anchorGravity="bottom|right|end"
        android:src="@drawable/ic_download"
        android:layout_margin="16dp"
        android:clickable="true"/>

How can I achieve this using a custom behaviour?

EJW
  • 338
  • 3
  • 6
  • 18
Fhl
  • 1,079
  • 1
  • 12
  • 26

4 Answers4

8

Based on what you want to achieve, you might want to try the SlidingUpPanelLayout (which can be found at Github). It has everything you seem to need:

  • Anchor: in order to achieve the anchors you decribe you'll have to either set the umanoAnchorPoint attribute (which is percentage based), or - if you need more complex anchor calculation, e.g. depending on the ImageView height - use the setAnchorPoint method.
  • Persistant: unlike the BottomSheets mentioned above, this layout is actually meant to be persistent. The height of the panel when collapsed can be customized with the umanoPanelHeight attribute or with the setPanelHeight method.
  • Parallax: not exactly sure whether the implementation fits your needs and if you require it, but it does have basic parallax support.

You can find more info in the Readme on Github which is linked above.

TR4Android
  • 3,200
  • 16
  • 21
5

What you've mentioned in the question is not actually a CollapsingToolbar but a stretchable BottomSheet. Unfortunately there is nothing offered by the android sdk yet for us to use (although Google use them in its own apps already).

BottomSheet is an Android component which presents a dismissible view from the bottom of the screen. BottomSheet can be a useful replacement for dialogs and menus but can hold any view so the use cases are endless. This repository includes the BottomSheet component itself but also includes a set of common view components presented in a bottom sheet. These are located in the commons module.

On the other hands, some nice guyz have been kind enough to develop close enough libraries and share them on GitHub for us to use. They are close to the BottomSheet component that Google uses.

The closest match to what you want is this one - Flipbard/BottomSheet - Check the images under "Common Components" section on this page. ( One plus point of this is that has been used in production at Flipboard for a while now so it is thoroughly tested.)

This is another minimal BottomSheet library - soarcn/BottomSheet, but it allows just clickable icons. You could probably get the source and customize it to suit your needs. But, the first one from Flipboard is a much closer match because it can hold any type of view and is more customizable.

Follow each of the above two GitHub links to see the samples, setup instructions and source code.

Viral Patel
  • 32,418
  • 18
  • 82
  • 110
  • Hi, thanks for helping out. I am familiar with BottomSheet (both the design and the two implementations you mention). The problem with Flipboard/BottomSheet is that it's not intended to be a persistent bottom sheet, which is what I require. Is there a way to get around that? It's a shame that google releases MD docs on BottomSheet, yet do not provide an implementation for it. – Fhl Jan 07 '16 at 12:31
  • To make it appear like it is persistent, you could load it on activity start, and when after selecting an option it dismisses, you could override the onDismiss method and reload it or if it allows just prevent dismissal. – Viral Patel Jan 07 '16 at 12:33
  • I'm afraid it's not possible to do that. https://github.com/Flipboard/bottomsheet/issues/66 "Hi @albertogarrido. This library currently doesn't have support for a sheet that is always open. All of the options you mentioned would have problems." – Fhl Jan 07 '16 at 12:41
2

I am not expert but take look at this implementation on Github.using modified Android SlidingUpPanel. implementing Foursquare like map animation.it might help you.

Joker
  • 537
  • 1
  • 6
  • 19
2

Update: Look at this answer if you are looking for all google maps behaviors

Answering your questions

How can I achieve this using a custom behavior? /Using custom behaviour to create multiple “anchor points”/positions...

What Google Maps use is a BottomSheetBehavior in app:layout_behavior to show the content that start in collapsed mode. It's not the map the one that got collapsed, Google maps has a backdrop image/toolbar with some parallax effect.

You can achieve what you want modifying default BottomSheetBehavior adding one more stat with following steps:

  1. Create a Java class and extend it from CoordinatorLayout.Behavior<V>
  2. Copy paste code from 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)



I'm going to add those modified methods and a link to the example project

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;
}



You can even use callbacks with behavior.setBottomSheetCallback(new BottomSheetBehaviorGoogleMapsLike.BottomSheetCallback() {....

I'm working on the image parallax effect and toolbar on those days.

And here is how its looks like:
[CustomBottomSheetBehavior]

Community
  • 1
  • 1
MiguelHincapieC
  • 5,445
  • 7
  • 41
  • 72