CollapsingToolbarLayout with a custom view

ChargerDukes picture ChargerDukes · Oct 1, 2015 · Viewed 28.5k times · Source

I'm trying to implement the CollapsingToolbarLayout with a custom view, but I'm unable to do it :

What I want to do (sorry I can't post images so it's on imgur) :

Expanded, the header is a profile screen with image and title

enter image description here

Not expanded (on scroll), the image and title will be on the toolbar

enter image description here

But everything I saw wasn't working as I expected

I'm new to this and lollipop animations so if someone could help me I'll be very grateful !

(I don't post sample code because I don't have something relevant to post)

Answer

hcpl picture hcpl · Jun 15, 2017

My Solution

I had the same scenario to implement so I started with the dog example and made some changes for it to work exactly like you describe. My code can be found as a fork on that project, see https://github.com/hanscappelle/CoordinatorBehaviorExample

enter image description here enter image description here

Most important changes are in the layout:

<android.support.design.widget.CoordinatorLayout
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/main.appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    >

    <android.support.design.widget.CollapsingToolbarLayout
        android:id="@+id/main.collapsing"
        android:layout_width="match_parent"
        android:layout_height="@dimen/expanded_toolbar_height"
        app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
        >

        <FrameLayout
            android:id="@+id/main.framelayout.title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            >

            <LinearLayout
                android:id="@+id/main.linearlayout.title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="bottom|center_horizontal"
                android:orientation="vertical"
                android:paddingBottom="@dimen/spacing_small"
                >

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:gravity="bottom|center_horizontal"
                    android:text="@string/tequila_name"
                    android:textColor="@android:color/white"
                    android:textSize="@dimen/textsize_xlarge"
                    />

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="@dimen/spacing_xxsmall"
                    android:text="@string/tequila_tagline"
                    android:textColor="@android:color/white"
                    />

            </LinearLayout>
        </FrameLayout>
    </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"
    android:scrollbars="none"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:lineSpacingExtra="@dimen/spacing_xsmall"
        android:padding="@dimen/spacing_normal"
        android:text="@string/lorem"
        android:textSize="@dimen/textsize_medium"
        />

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

<android.support.v7.widget.Toolbar
    android:id="@+id/main.toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/primary"
    app:layout_anchor="@id/main.collapsing"
    app:theme="@style/ThemeOverlay.AppCompat.Dark"
    app:title=""
    >

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        >

        <Space
            android:layout_width="@dimen/image_final_width"
            android:layout_height="@dimen/image_final_width"
            />

        <TextView
            android:id="@+id/main.textview.title"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="8dp"
            android:gravity="center_vertical"
            android:text="@string/tequila_title"
            android:textColor="@android:color/white"
            android:textSize="@dimen/textsize_large"
            />

    </LinearLayout>
</android.support.v7.widget.Toolbar>

<de.hdodenhof.circleimageview.CircleImageView
    android:layout_width="@dimen/image_width"
    android:layout_height="@dimen/image_width"
    android:layout_gravity="top|center_horizontal"
    android:layout_marginTop="@dimen/spacing_normal"
    android:src="@drawable/ninja"
    app:border_color="@android:color/white"
    app:border_width="@dimen/border_width"
    app:finalHeight="@dimen/image_final_width"
    app:finalXPosition="@dimen/spacing_small"
    app:finalYPosition="@dimen/spacing_small"
    app:finalToolbarHeight="?attr/actionBarSize"
    app:layout_behavior="saulmm.myapplication.AvatarImageBehavior"
    />

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

And in the AvatarImageBehaviour class that I optimised for only moving the avatar from the original position to the position configured in the attributes. So if you want the image to move from another location just move it within the layout. When you do so make sure the AppBarLayout is still a sibling of it or it won't be found in code.

package saulmm.myapplication;

import android.content.Context;
import android.content.res.TypedArray;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.CoordinatorLayout;
import android.util.AttributeSet;
import android.view.View;
import de.hdodenhof.circleimageview.CircleImageView;

public class AvatarImageBehavior extends CoordinatorLayout.Behavior<CircleImageView> {

// calculated from given layout
private int startXPositionImage;
private int startYPositionImage;
private int startHeight;
private int startToolbarHeight;

private boolean initialised = false;

private float amountOfToolbarToMove;
private float amountOfImageToReduce;
private float amountToMoveXPosition;
private float amountToMoveYPosition;

// user configured params
private float finalToolbarHeight, finalXPosition, finalYPosition, finalHeight;

public AvatarImageBehavior(
        final Context context,
        final AttributeSet attrs) {

    if (attrs != null) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AvatarImageBehavior);
        finalXPosition = a.getDimension(R.styleable.AvatarImageBehavior_finalXPosition, 0);
        finalYPosition = a.getDimension(R.styleable.AvatarImageBehavior_finalYPosition, 0);
        finalHeight = a.getDimension(R.styleable.AvatarImageBehavior_finalHeight, 0);
        finalToolbarHeight = a.getDimension(R.styleable.AvatarImageBehavior_finalToolbarHeight, 0);
        a.recycle();
    }
}

@Override
public boolean layoutDependsOn(
        final CoordinatorLayout parent,
        final CircleImageView child,
        final View dependency) {

    return dependency instanceof AppBarLayout; // change if you want another sibling to depend on
}

@Override
public boolean onDependentViewChanged(
        final CoordinatorLayout parent,
        final CircleImageView child,
        final View dependency) {

    // make child (avatar) change in relation to dependency (toolbar) in both size and position, init with properties from layout
    initProperties(child, dependency);

    // calculate progress of movement of dependency
    float currentToolbarHeight = startToolbarHeight + dependency.getY(); // current expanded height of toolbar
    // don't go below configured min height for calculations (it does go passed the toolbar)
    currentToolbarHeight = currentToolbarHeight < finalToolbarHeight ? finalToolbarHeight : currentToolbarHeight;
    final float amountAlreadyMoved = startToolbarHeight - currentToolbarHeight;
    final float progress = 100 * amountAlreadyMoved / amountOfToolbarToMove; // how much % of expand we reached

    // update image size
    final float heightToSubtract = progress * amountOfImageToReduce / 100;
    CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) child.getLayoutParams();
    lp.width = (int) (startHeight - heightToSubtract);
    lp.height = (int) (startHeight - heightToSubtract);
    child.setLayoutParams(lp);

    // update image position
    final float distanceXToSubtract = progress * amountToMoveXPosition / 100;
    final float distanceYToSubtract = progress * amountToMoveYPosition / 100;
    float newXPosition = startXPositionImage - distanceXToSubtract;
    //newXPosition = newXPosition < endXPosition ? endXPosition : newXPosition; // don't go passed end position
    child.setX(newXPosition);
    child.setY(startYPositionImage - distanceYToSubtract);

    return true;
}

private void initProperties(
        final CircleImageView child,
        final View dependency) {

    if (!initialised) {
        // form initial layout
        startHeight = child.getHeight();
        startXPositionImage = (int) child.getX();
        startYPositionImage = (int) child.getY();
        startToolbarHeight = dependency.getHeight();
        // some calculated fields
        amountOfToolbarToMove = startToolbarHeight - finalToolbarHeight;
        amountOfImageToReduce = startHeight - finalHeight;
        amountToMoveXPosition = startXPositionImage - finalXPosition;
        amountToMoveYPosition = startYPositionImage - finalYPosition;
        initialised = true;
    }
}
}

Sources

Most common example is the one with the dog listed at https://github.com/saulmm/CoordinatorBehaviorExample . It's a good example but indeed has the toolbar in the middle of the expanded view with a backdrop image that also moves. All that was removed in my example.

Another explanation is found at http://www.devexchanges.info/2016/03/android-tip-custom-coordinatorlayout.html but since that cloud/sea backdrop image referenced there is also found in the dog example one is clearly build on top of the other.

I also found this SO question with a bounty awarded but couldn't really find out what the final solution was Add icon with title in CollapsingToolbarLayout

And finally this should be a working library that does the work. I've checked it out but the initial image wasn't centered and I rather worked on the dog example that I had looked at before. See https://github.com/datalink747/CollapsingAvatarToolbar

More to read

http://saulmm.github.io/mastering-coordinator http://www.androidauthority.com/using-coordinatorlayout-android-apps-703720/ https://developer.android.com/reference/android/support/design/widget/CoordinatorLayout.html https://guides.codepath.com/android/handling-scrolls-with-coordinatorlayout