CollapsingToolbarLayout with fixed/pinned Toolbar

2020-04-19 08:35发布

I have a standard "CollapsingToolbarLayout" implementation based on material design guidelines.

With the the settings below I was able to achieve behavior depicted on the picture:

<CoordinatorLayout ...>
    <AppBarLayout ...>
        <CollapsingToolbarLayout
            app:layout_scrollFlags="scroll|enterAlways"
            ...>
            <Toolbar
                app:layout_collapseMode="pin">
            </Toolbar>
            <MyCustomContent01 ... />
        </CollapsingToolbarLayout>
    </AppBarLayout>
    <MyCustomContent02 ... />
</CoordinatorLayout>

enter image description here

Question

How to achieve the following behavior?:

  • Scrolling up: always expand the Toolbar fully, even if we are not on the top of the list.
  • Scrolling down: just collapse the Toolbar, but don't hide it.

In other words: How can I get rid of the step 3 while preserving the condition of step 4?

enter image description here

Research

For me the best article on this topic seems to be this one, however none of the presented configurations match my needs.

Attempt one:

<CollapsingToolbarLayout
    app:layout_scrollFlags="scroll|enterAlways"
 ...>
  • Never hide fully the Toolbar (getting rid of step 3): FAIL
  • Expand the Toolbar even if we are not on the top: OK

Attempt two

<CollapsingToolbarLayout
    app:layout_scrollFlags="scroll|exitUntilCollapsed"
 ...>
  • Never hide fully the Toolbar (getting rid of step 3): OK
  • Expand the Toolbar even if we are not on the top: FAIL

2条回答
我只想做你的唯一
2楼-- · 2020-04-19 08:41

The default CollapsingToolbarLayout provided by the Material library is too limited to a set of predefined animations. To create our own custom effects, say you want to fully expand the header layout when scrolling up on the RecyclerView/ NestedScrollView even if you are not on top of the scroll view, you can use the powerful MotionLayout which is a subclass of ConstraintLayout for building animations. If you are happy to replace the existing view hierarchy with a flat Constraint-layout equivalent, read the detailed answer given below.


Here I am going to show you how to create the "enterAlways" effect with an always pinned header layout, all with in three simple steps.

Before writing any code, see the gif image given below to better understand what we are trying to create.

custom toolbar animation using motion layout

1. Add the ConstraintLayout dependency:

To use MotionLayout in your project, add the ConstraintLayout 2.0 dependency to your app's build.gradle file. If you're using AndroidX, add the following dependency:

dependencies {
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
}

If you aren't using AndroidX, add the following support library dependency:

dependencies {
    implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta2'
}

2. Create the MotionLayout file:

MotionLayout is a subclass of ConstraintLayout, so you can transform any existing ConstraintLayout into a MotionLayout. So create one layout file as shown below.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
motion:layoutDescription="@xml/motionscene"
tools:showPaths="false">


<View
    android:id="@+id/toolbar_image"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:background="#345634"
    android:fitsSystemWindows="true"
    motion:layout_constraintBottom_toBottomOf="@id/toolbar"
    motion:layout_constraintEnd_toEndOf="parent"
    motion:layout_constraintStart_toStartOf="parent"
    motion:layout_constraintTop_toTopOf="parent" />

<ImageView
    android:id="@+id/home"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:src="@drawable/abc_ic_ab_back_material"
    android:tint="?android:attr/textColorPrimaryInverse"
    motion:layout_constraintStart_toStartOf="parent"
    motion:layout_constraintTop_toTopOf="parent" />

<LinearLayout
    android:id="@+id/customHeader"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    motion:layout_constraintEnd_toEndOf="parent"
    motion:layout_constraintStart_toEndOf="@id/home"
    motion:layout_constraintTop_toTopOf="parent">

    <LinearLayout
        android:id="@+id/row1"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="16dp"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="8dp">

        <View
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:background="#345678" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:text="some text"
            android:textColor="#ffffff" />
    </LinearLayout>


    <LinearLayout
        android:id="@+id/row2"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="16dp"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="8dp">

        <View
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:background="#345678" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:text="some text"
            android:textColor="#ffffff" />
    </LinearLayout>

    <LinearLayout
        android:id="@+id/row3"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_marginLeft="16dp"
        android:layout_marginTop="8dp"
        android:layout_marginRight="16dp"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="8dp">

        <View
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:background="#345678" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:text="some text"
            android:textColor="#ffffff" />
    </LinearLayout>


</LinearLayout>


<LinearLayout
    android:id="@+id/toolbar"
    android:layout_width="0dp"
    android:layout_height="40dp"
    android:layout_marginStart="16dp"
    android:layout_marginLeft="16dp"
    android:layout_marginTop="8dp"
    android:background="#345634"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="8dp"
    motion:layout_constraintEnd_toEndOf="parent"
    motion:layout_constraintStart_toEndOf="@id/home"
    motion:layout_constraintTop_toBottomOf="@id/customHeader">

    <View
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:background="#F44336" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="16dp"
        android:text="Toolbar Title"
        android:textColor="#ffffff" />

    <View
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_marginLeft="16dp"
        android:background="#F44336" />
</LinearLayout>

<androidx.core.widget.NestedScrollView
    android:id="@+id/scrollView"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:background="#ffffff"
    motion:layout_constraintBottom_toBottomOf="parent"
    motion:layout_constraintEnd_toEndOf="parent"
    motion:layout_constraintStart_toStartOf="parent"
    motion:layout_constraintTop_toBottomOf="@+id/toolbar_image">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="@dimen/text_margin"
        android:text="@string/large_text" />

</androidx.core.widget.NestedScrollView>

</androidx.constraintlayout.motion.widget.MotionLayout>

3. Create a MotionScene:

In the previous step, motion:layoutDescription attribute references a MotionScene. A MotionScene is an XML resource file that contains all of the motion descriptions for the corresponding layout. To keep layout information separate from motion descriptions, each MotionLayout references a separate MotionScene. Note that definitions in the MotionScene take precedence over any similar definitions in the MotionLayout.

Following is a sample MotionScene file that creates the required fixed/pinned toolbar with 'enterAlways' effect:

Put the file in the xml folder under the res directory.

motionscene.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
    motion:constraintSetEnd="@id/collapsed"
    motion:constraintSetStart="@id/expanded">

    <OnSwipe
        motion:dragDirection="dragUp"
        motion:moveWhenScrollAtTop="false"
        motion:touchAnchorId="@id/scrollView"
        motion:touchAnchorSide="top" />

</Transition>

<ConstraintSet android:id="@+id/expanded">
    <Constraint android:id="@id/toolbar_image" />
    <Constraint android:id="@id/toolbar" />
    <Constraint android:id="@id/customHeader">
        <PropertySet android:alpha="1" />
    </Constraint>
</ConstraintSet>

<ConstraintSet android:id="@+id/collapsed">
    <Constraint
        android:id="@id/toolbar_image"
        android:layout_height="?attr/actionBarSize"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toStartOf="parent"
        motion:layout_constraintTop_toTopOf="parent">

    </Constraint>
    <Constraint
        android:id="@id/customHeader"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toEndOf="@id/home"
        motion:layout_constraintTop_toTopOf="parent">
        <PropertySet android:alpha="0" />
    </Constraint>

    <Constraint
        android:id="@id/toolbar"
        android:layout_height="?attr/actionBarSize"
        android:layout_marginStart="16dp"
        android:layout_marginTop="0dp"
        motion:layout_constraintEnd_toEndOf="parent"
        motion:layout_constraintStart_toEndOf="@id/home"
        motion:layout_constraintTop_toTopOf="parent">

    </Constraint>

</ConstraintSet>


</MotionScene>

That's all. You created an amazing custom animation with out writing any java/kotlin code. MotionLayout is fully declarative, meaning that you can describe any transitions in XML, no matter how complex.


The following repo by Google includes more samples.

https://github.com/googlesamples/android-ConstraintLayoutExamples

查看更多
姐就是有狂的资本
3楼-- · 2020-04-19 08:48

you have to combine both flags to get that effect

Try this

app:layout_scrollFlags="scroll|exitUntilCollapsed|enterAlways"
查看更多
登录 后发表回答