Reputation: 1193
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.
Preferably, the app should snap between these positions.
As of now I have the layout basically working.
app:layout_behavior="android.support.design.widget.AppBarLayout$ScrollingViewBehavior"
. I believe this is a bug with AppBarLayout
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
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:
CoordinatorLayout.Behavior<V>
BottomSheetBehavior
file to your new one.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);
}
Add a new state
public static final int STATE_ANCHOR_POINT = X;
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:
[]
Upvotes: 2
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
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:
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.umanoPanelHeight
attribute or with the setPanelHeight
method.You can find more info in the Readme on Github which is linked above.
Upvotes: 8
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