Fhl
Fhl

Reputation: 1193

Using custom behaviour to create multiple "anchor points"/positions for CollapsingToolbar

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.

Fullscreen

Half way

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:

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?

Upvotes: 37

Views: 4457

Answers (4)

MiguelHincapieC
MiguelHincapieC

Reputation: 5531

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]

Upvotes: 2

Joker
Joker

Reputation: 545

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.

Upvotes: 2

TR4Android
TR4Android

Reputation: 3246

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.

Upvotes: 8

Viral Patel
Viral Patel

Reputation: 33438

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.

Upvotes: 5

Related Questions