Skip to content

Experiment #4: Expanding an image with MotionLayout

Adriel Café edited this page Aug 24, 2020 · 4 revisions

That was my first time using MotionLayout, nothing too complex but a solid way to start. The animation I did consists on expand (to fullscreen) and shrink (to its original size) an ImageView. Here's the final result:

To achieve this animation, I use a single ImageView on top of a RecyclerView. When a wallpaper is selected I manually place the ImageView on top of the selected wallpaper (with the same size and aspect ratio) and start the expanding animation.

Let's see, step by step, how to implement this animation:

Step 1: the layout

The MotionLayout must contain a RecyclerView and an ImageView:

<androidx.constraintlayout.motion.widget.MotionLayout
    app:layoutDescription="@xml/wallpaper_scene">

    <androidx.recyclerview.widget.RecyclerView/>

    <androidx.appcompat.widget.AppCompatImageView/>

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

Full implementation on section_wallpapers.xml

Step 2: the scene

In the MotionScene we need to declare two ConstraintSets (for the start and end animations) and a Transition (to specify the ConstraintSets, animation duration and other attributes).

The animation should start with the ImageView invisible. The value of width and height will be defined at runtime (explained in the next step).

The animation should end with the ImageView visible. The value of width and height must be match_parent.

<MotionScene
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetStart="@id/start"
        app:constraintSetEnd="@+id/end"/>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/currentWallpaper">
            <PropertySet android:visibility="invisible"/>
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@id/currentWallpaper"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <PropertySet android:visibility="visible"/>
        </Constraint>
    </ConstraintSet>
</MotionScene>

Full implementation on wallpaper_scene.xml

Step 3: positioning the ImageView

Let's start by creating an extension property which returns the coordinates of any View in the window:

val View.windowLocation: PointF
    get() {
        val (x, y) = IntArray(2)
            .also(::getLocationInWindow)
            .map(Int::toFloat)

        return PointF(x, y)
    }

Now we get the coordinates of the selected wallpaper and change the translationX and translationY values of the ImageView used in the start ConstraintSet:

val itemWindowLocation = itemBinding.image.windowLocation

sectionBinding.root
    .getConstraintSet(R.id.start)
    .setTranslation(R.id.currentWallpaper, itemWindowLocation.x, itemWindowLocation.y)

Full implementation on WallpaperExpanderHelper.kt

Step 4: expanding and shrinking

It's done! We can now control the animation programmatically:

// Expanding
sectionBinding.root.transitionToEnd()

// Shrinking
sectionBinding.root.transitionToStart()

Conclusion

MotionLayout is very powerful and simpler to use than I thought. I'll definitely dive into it from now on.