MotionLayout: Dynamic toolbar

Posted by CapEsiouS on Wed, 28 Aug 2019 18:06:55 +0200

Regular readers of Styling Android may have guessed what I like about animation.MotionLayout provides an amazing range of animations and can be used to create some very interesting ones.We've seen it before on Styling Android how to implement a Collapsing Toolbar But instead of just mimicking existing behavior that can be implemented using other Android API s, MotionLayout gives us some real scope to get fashionable.In this article, we'll break a few lines and explore some interesting techniques we can use with MotionLayout. original text

Before we start, it's worth noting that it's easy to move beyond the top in animation.Although it's easy to add more and more complex components to an animation, sometimes knowing when to stop adding other components or even delete those that don't fit into the overall process is the key to making an animation feel natural.In addition, the most effective animations are usually those that combine very simple component animations that complement each other and create animations that are more difficult to achieve than they really are.

Let's first look at the overall effect we're going to achieve.This is a toolbar that may appear in a sports application that displays information about an ongoing football match (American football match):

(I should point out that I am not distressed by the results of the 2019 F.A. Cup final.None at all.)

Although there are a lot of things going on in the animation, the actions are all together, so the overall effect feels very smooth.The actual green rectangular block Toolbar does not actually change size, but the current Match Time text (89.59) moves outside the boundary of the Toolbar and has a bubble shape that extends from the bottom of the Toolbar to contain it.This is probably the most interesting part of the animation, so most of this article will focus on this.

I do not intend to fully describe MotionLayout's mechanism because previous article Already covered.The key is that we have effectively defined two statics, each represented as a ConstraintSet.The expanded state is as follows:

A crashed state looks like this:

MotionLayout itself declares the following:

RES/Layout/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:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/collapsing_toolbar"
    tools:context=".MainActivity">

    <View
        android:id="@+id/toolbar"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@color/colorPrimary" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/event_name"
        android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" />

    <TextView
        android:id="@+id/score"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/score"
        android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" />

    <ImageView
        android:id="@+id/man_city_logo"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:adjustViewBounds="true"
        android:contentDescription="@null"
        android:paddingStart="16dp"
        android:paddingEnd="16dp"
        android:src="@drawable/man_city" />

    <TextView
        android:id="@+id/man_city"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/man_city"
        android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
        app:layout_constraintBaseline_toBaselineOf="@id/score"
        app:layout_constraintEnd_toStartOf="@id/man_city_logo"/>

    <ImageView
        android:id="@+id/watford_logo"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:adjustViewBounds="true"
        android:contentDescription="@null"
        android:paddingStart="16dp"
        android:paddingEnd="16dp"
        android:src="@drawable/watford" />

    <TextView
        android:id="@+id/watford"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/watford"
        android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse"
        app:layout_constraintBaseline_toBaselineOf="@id/score"
        app:layout_constraintStart_toEndOf="@id/watford_logo"/>

    <View
        android:id="@+id/toolbar_extension"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/bubble"
        android:backgroundTint="@color/colorPrimary"
        app:layout_constraintEnd_toEndOf="@id/time"
        app:layout_constraintStart_toStartOf="@id/time" />

    <TextView
        android:id="@+id/time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingStart="24dp"
        android:paddingEnd="24dp"
        android:text="@string/time"
        android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" />

    <FrameLayout
        android:id="@+id/recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/toolbar_extension" />
</androidx.constraintlayout.motion.widget.MotionLayout>

Although MotionLayout is a subclass of ConstraintLayout, there are no constraints declared in this layout file - they are all declared in the layoutDescriptor file named @xml/collapsing_toolbar.

This file contains our MotionLayout's MotionScene:

RES/XML/collapsing_toolbar.xml


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

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

        <OnSwipe
            app:dragDirection="dragUp"
            app:touchAnchorId="@id/recyclerview"
            app:touchAnchorSide="top" />

    </Transition>

    <ConstraintSet android:id="@+id/collapsed">
        ...
    </ConstraintSet>

    <ConstraintSet android:id="@+id/expanded">
        ...
    </ConstraintSet>

</MotionScene>

This will declare extended and collapsed constraints in its own ConstraintSet and associate them with a drag gesture to drag the transition between the two states.For those unfamiliar with this point: earlierMotionLayoutarticle This is described in more detail.

Most of the animations we use for each view are scaling them.If you look at earlier animation GIF s that show the appearance of the animation and focus on the top text F.A. Cup Final 2019, it just gets smaller and smaller, and the constraints in ConstraintSets are:

RES/XML/collapsing_toolbar.xml


<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    ...
    <ConstraintSet android:id="@+id/collapsed">
        ...
        <Constraint android:id="@id/title">
            <Transform
                android:scaleX="0.5"
                android:scaleY="0.5" />
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toTopOf="@id/score"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="@id/toolbar" />
        </Constraint>
        ...
    </ConstraintSet>

    <ConstraintSet android:id="@+id/expanded">
        ...
        <Constraint android:id="@id/title">
            <Transform
                android:scaleX="1.0"
                android:scaleY="1.0" />
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
        ...
    </ConstraintSet>

</MotionScene>

Although there are some minor differences in the layout_constraint* property, the key here is Transform, which scales the text between the collpased and expanded states.MotionLayout will automatically zoom in and out for us.What we get for free is that if we restrict other views to the bottom of the View, they will move as the zoom applies to this View.So the visual effect is that the view below moves as it grows and shrinks.See how team names and scores move with the size of the title text in the GIF.

We use the same technique to scale the team logo and matching time text (89:59).I'm not going to cover them separately, but check them out accompanying source code To view this content.

But the matching time text is worth looking at:

RES/XML/collapsing_toolbar.xml


<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    ...
    <ConstraintSet android:id="@+id/collapsed">
        ...
        <Constraint android:id="@id/toolbar">
            <Layout
                android:layout_width="0dp"
                android:layout_height="?attr/actionBarSize"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
        ...
        <Constraint android:id="@id/time">
            <Transform
                android:scaleX="0.5"
                android:scaleY="0.5" />
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="@id/toolbar"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/score" />
        </Constraint>
        ...
    </ConstraintSet>

    <ConstraintSet android:id="@+id/expanded">
        ...
        <Constraint android:id="@id/toolbar">
            <Layout
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:layout_constraintBottom_toTopOf="@id/time"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>
        ...
        <Constraint android:id="@id/time">
            <Transform
                android:scaleX="1.0"
                android:scaleY="1.0" />
            <Layout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/score"
                android:layout_marginTop="8dp"/>
        </Constraint>
        ...
    </ConstraintSet>

</MotionScene>

The same type of zoom is in progress, but in the collapsed state it is inside the Toolbar, but in the expanded state it is below.

On its own, this does not work well for extended states because the text itself is light and the background beneath the Toolbar is light.Therefore, we need to lower the green "foam" below the Toolbar to ensure that the text has a contrasting background.

The foam itself is a VectorDrawable:

RES/Pull/bubble.xml


<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="48dp"
    android:height="16dp"
    android:viewportWidth="150"
    android:viewportHeight="50">
    <path
        android:fillColor="@android:color/white"
        android:pathData="M0,0 a 25,25 0 0 1 25,25 a 25,25 0 0 0 25,25 h 50 a 25,25 0 0 0 25,-25 a 25,25 0 0 1 25,-25 Z" />
</vector>

It consists of arcs and horizontal lines that actually look like this:

Shapes should be visible in the static expanded state image we saw earlier, and green should be applied to the layout.What makes it interesting is how to apply it in Motion Scene:

RES/XML/collapsing_toolbar.xml


<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    ...
    <ConstraintSet android:id="@+id/collapsed">
        ...
        <Constraint android:id="@id/toolbar_extension">
            <Layout
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:layout_constraintBottom_toBottomOf="@id/toolbar"
                app:layout_constraintEnd_toEndOf="@id/time"
                app:layout_constraintStart_toStartOf="@id/time"
                app:layout_constraintTop_toBottomOf="@id/toolbar" />
        </Constraint>
        ...
    </ConstraintSet>

    <ConstraintSet android:id="@+id/expanded">
        ...
        <Constraint android:id="@id/toolbar_extension">
            <Layout
                android:layout_width="0dp"
                android:layout_height="0dp"
                app:layout_constraintBottom_toBottomOf="@id/time"
                app:layout_constraintEnd_toEndOf="@id/time"
                app:layout_constraintStart_toStartOf="@id/time"
                app:layout_constraintTop_toTopOf="@id/time" />
        </Constraint>
        ...
    </ConstraintSet>

</MotionScene>

In a collapsed state, the top and bottom of this View are constrained to the bottom of the Toolbar.The result has no height.

In the expanded state, the top and bottom of this View are constrained to the top and bottom of the matching time TextView.Its height matches the matching time TextView.

As MotionLayout transitions between the collapsed and expanded states, the height of the bubbles changes, creating the illusion that bubbles grow from the bottom of the Toolbar.We can actually get a subtle different effect, and we keep the foam consistent with the matching time TextView.But I personally prefer this because it makes the bubbles look bigger instead of slipping out of the Toolbar - I think it feels more organic.

We're done.If we put all this together, we get the following results:

This particular implementation may not be appropriate in some cases because the matching time will cover anywhere below the Toolbar, but I think the Shape Change Toolbar extension is an interesting idea, and that's why I'm sharing it here.

here The source code for this article is provided.

Topics: Android xml encoding