Introducción a MotionLayout

Desde siempre, los desarrolladores de Android hemos tenido dificultades a la hora de realizar animaciones en nuestras aplicaciones. Las librerías nativas de Android no solían tener facilidades para crear simples animaciones, como el “swipe” (deslizar) para eliminar un objeto de un listado. Estos nos obligaba a crear una vista personalizada que detectará el gesto del usuario y nosotros mismos usar ese “input” del usuario para ir moviendo el objeto y a la vez aplicarle la lógica de que desapareciese en un momento preciso.

En los últimos años, con la incorporación de Material Design, se han ido aplicando cambios a las librerías desde el CoordinatorLayout para tener una vista con una sección qué se pudiera colapsar fácilmente a ConstraintLayout dónde podemos asignar a una vista anclajes a extremos tanto de la pantalla como de otros elementos de la vista, evitando tener una vista agrupada en bloques.

En este último aporte, desde finales de 2018, la librería nativa se actualizó a su versión 2.0 y con ello, MotionLayout, una clase que extiende de ConstraintLayout que nos permite animar nuestros elementos dentro del “layout” a través de una escena, archivo XML, dónde describiremos el comportamiento de la animación de inicio a fin, similar a una animación vectorial en programas de diseño, y con que eventos se inicia esta animación (clickar, deslizar, arrastrar…)

Paso a paso

En muchas aplicaciones, habréis visto la típica vista de un perfil o categoría, dónde tenemos una imagen grande y un texto asociado, que posteriormente al realizar scroll en la pantalla se reduce hacia una esquina con el texto al lado.

Anteriormente para hacer esto era un agobio, pero con MotionLayout veremos una manera fácil de hacerlo usando las escenas.

Empezaremos creando el proyecto y añadiendo la librería de constraintLayout a nuestro archivo app.gradle usando una de las siguientes líneas:

// Support library
implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta2'

// or AndroidX packages
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'

En este ejemplo, usaremos la librería de AndroidX pero podéis usar la que más os convenga.

Tras aplicar la librería lo siguiente será crear la vista que queremos animar. Como mencionaba al inicio, haremos una vista anclada a un listado, un header, donde sus elementos se moverán en función del scroll que hagamos en el listado. Para ello en nuestra vista haremos que MotionLayout sea el parent group de nuestra vista e incorporaremos los elementos asociados.

<?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">

    <View
    android:id="@+id/background"
    android:layout_width="match_parent"
    android:layout_height="320dp"
    android:background="@color/colorPrimary"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <ImageView
    android:id="@+id/imageView"
    android:layout_width="180dp"
    android:layout_height="180dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:src="@tools:sample/avatars" />

  <TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="12dp"
    android:textSize="24sp"
    android:textStyle="bold"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/imageView"
    tools:text="@tools:sample/full_names" />

  <TextView
    android:id="@+id/textView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:textSize="18sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/textView"
    tools:text="@tools:sample/cities" />

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

Por lo pronto, lo que tenemos es un layout aplicando unas constraints como haríamos en un ConstraintLayout, solo que hemos usado MotionLayout en su lugar que nos permitirá asociarle una escena. Estas constraints funcionarán como si fuera el estado inicial de nuestra vista antes de iniciar el gesto para ejecutar la animación.

Ahora le incluiremos un listado, para ello usaré RecyclerView que esta incluido en la librería de Material Design que la incluiremos con una de estas líneas:

// Support library
implementation 'com.android.support:design:28.0.0'

// or AndroidX
implementation 'com.google.material:material:1.1.0'

Incluiremos el listado dentro del MotionLayout que hemos creado anteriormente y con ello ya estaríamos listos para crear nuestra animación. Para ello, crearemos una escena dónde el root sea MotionScene, que es un archivo XML dónde especificaremos la transición desde la posición inicial a la final de nuestros elementos durante ese gesto, en nuestro caso el scroll del listado.

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

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

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

    </Transition>

</MotionScene>

Los atributos constraintSetStart y constraintSetEnd hacen referencia a los estados inicial y final que queremos que tenga nuestra animación.

La etiqueta OnSwipe nos permite designar que la transición se ejecutará con este tipo de gesto. El atributo dragDirection nos indicará la dirección del swipe para efectuarse la animación y los atributos touchAnchorId y touchAnchorSide hacen referencia al listado dónde haremos scroll y desde que zona se inicia el gesto, en este caso top.

Además tendremos que crear un set de constraints para el estado inicial y final de nuestra animación, dónde cada constraint hará referencia al identificador de nuestro elemento en el layout.

<ConstraintSet android:id="@+id/start">
    <Constraint android:id="@+id/background">
      <PropertySet android:alpha="1" />
    </Constraint>

  </ConstraintSet>

<ConstraintSet android:id="@+id/end">
        <Constraint
      android:id="@+id/background"
      android:layout_height="80dp"
      android:alpha="0"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

    <Constraint
      android:id="@+id/imageView"
      android:layout_width="64dp"
      android:layout_height="64dp"
      android:layout_marginStart="8dp"
      app:layout_constraintBottom_toBottomOf="@id/background"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />
...
</ConstraintSet>

En cada set, tendremos que identificar que elemento queremos modificar a través de la etiqueta Constraint y dentro asignar el atributo deseado, por ejemplo nuestro imageView que en el estado final queremos que reduzca de tamaño, además que ya no tiene el atributo layout_constraintEnd_ToEndOf para que quede a la izquierda.

Además también se puede modificar estados de la visibilidad usando dentro de la etiqueta Constraint la etiqueta PropertySet dónde podemos asignar el atributo alpha al fondo de nuestro header que al final de nuestra animación queremos que desaparezca.

Listo! Con MotionLayout podemos crear un listado con su header colapsado de una manera eficiente y con animaciones en sus elementos.

Para ver el ejemplo completo, https://github.com/raeve/motionlayout-example