Animaciones En El ConstraintLayout

En este tutorial veremos cómo crear animaciones en el ConstraintLayout a partir de las clases ConstraintSet y TransitionManager.

La clase ConstraintSet representa la definición de un conjunto de restricciones para el ConstraintLayout. Esto te permitirá crearlas, guardarlas y aplicarlas programáticamente desde Kotlin.

Por otro lado, la clase TransitionManager se encarga de manejar los objetos de transición que son asociados a una escena. La usaremos para hacer que el ConstraintLayout vaya de un conjunto restricciones hacia otro y así proyectar la animación.

Nota: Esta es la quinta parte de la guía del ConstraintLayout en Android (todo). Te recomiendo leer los anteriores apartados en secuencia para comprender mejor este contenido.


Ejemplo De Animaciones En El ConstraintLayout

Tomaremos como ilustración un layout con dos TextViews y un botón encargado de ordenar su traslación.

Ejemplo de animaciones en el ConstraintLayout en Android

Cuando el usuario presione el botón Mover, desplazamos hacia el extremo contrario a las Imágenes. Quien se encuentre en la parte superior tendrá un tamaño mayor.

La idea es probar la animación de la posición y el tamaño de los componentes en la interpolación. Puedes descargar el código desde el siguiente enlace:

Crear Fotograma Inicial

Para construir una animación a partir de ConstraintSets es necesario definir dos layouts que representen los fotogramas. Es decir, a partir de un layout inicial, el framework usará una interpolación que cambie la posición y dimensiones de cada view, hasta transicionar al layout final.

En nuestro caso el diseño de primer layout es la organización de tres widgets:

  • ImageView con icono de barco
  • ImageView con icono de auto
  • Botón Mover
Layout para fotograma inicial de ConstraintLayout

Partiendo de estos requisitos, crea un nuevo layout llamado p5_start_frame.xml con un ConstraintLayout como raíz. Y luego posiciona los views necesarios para cada elemento. El resultado sería algo similar a esto:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/constraintlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_boat"
        android:layout_width="44dp"
        android:layout_height="44dp"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:src="@drawable/ic_boat"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/move_button" />

    <Button
        android:id="@+id/move_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Mover"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_car"
        android:layout_width="88dp"
        android:layout_height="88dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:src="@drawable/ic_car"
        app:layout_constraintBottom_toTopOf="@+id/move_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

En seguida ve a MainActivity y asígnalo con setContentActivity() en onCreate():

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.p5_start_frame)
    }
}

Crear Fotograma Final

La animación termina desplazando a la posición contraria del eje vertical a los ImageViews. Además se incrementa el tamaño del icono que se encuentre en la parte superior.

Layout para fotograma final de ConstraintLayout

Con esto en mente, crea un nuevo layout llamado p5_end_frame.xml para representar el frame final. La definición con las posiciones finales será la siguiente:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/constraintlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/iv_boat"
        android:layout_width="88dp"
        android:layout_height="88dp"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:src="@drawable/ic_boat"
        app:layout_constraintBottom_toTopOf="@+id/move_button"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/move_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Mover"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv_car"
        android:layout_width="44dp"
        android:layout_height="44dp"
        android:layout_marginEnd="16dp"
        android:layout_marginRight="16dp"
        android:src="@drawable/ic_car"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/move_button" />

</androidx.constraintlayout.widget.ConstraintLayout>

Iniciar Transición

Para iniciar la animación usaremos la función de extensión TransitionManager.beginDelayedTransition() cuando el botón de agendar sea presionado.

No obstante, es necesario definir el segundo frame de la animación desde p5_end_frame.xml. Esto consiste en:

  1. Crear una instancia de ConstraintSet con su constructor público
  2. Cargar las restricciones con su método load() o clone()
  3. Aplicar las restricciones del conjunto al ConstraintLayout con applyTo()

Teniendo esto en cuenta comencemos por crear las propiedades de la actividad. Como vamos a alternar entre ambos frames, declararemos dos conjuntos de restricciones para sostener las reglas de ambos layouts.

private lateinit var constraintLayout: ConstraintLayout

private var initialState = true
private val startConstraints = ConstraintSet()
private val endConstraints = ConstraintSet()

La centinela initialState permitirá intercambiar entre ambos conjuntos al momento de presionar el botón.

Lo siguiente es inicializar los conjuntos y setear la escucha del botón de movimiento:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.p5_start_frame)

    constraintLayout = findViewById(R.id.constraintlayout)

    startConstraints.clone(constraintLayout)
    endConstraints.load(this, R.layout.p5_end_frame)

    findViewById<TextView>(R.id.move_button).setOnClickListener {
        moveIcons()
    }
}

Para startConstraints usamos el método clone(), ya que clona las restricciones actuales del p5_start_frame.xml con el fin de restablecer el estado inicial. Pero por el lado de endConstraints usamos load() para obtener el conjunto de p5_end_frame.xml.

Finalizamos implementando la lógica de moveIcons() para comenzar la animación con el TransitionManager:

private fun moveIcons() {
    val currentConstraints = if (initialState) endConstraints else startConstraints
    initialState = !initialState

    TransitionManager.beginDelayedTransition(constraintLayout)

    currentConstraints.applyTo(constraintLayout)
}

La primera acción es determinar cuál es el conjunto que debe ser aplicado actualmente basado en el valor de la bandera initialState. Luego actualizamos el valor de la misma para el futuro cambio.

En seguida invocamos a beginDelayedTransition() pasando al ConstraintLayout como referencia y terminamos usando applyTo() sobre él, con el fin de aplicar el nuevo conjunto de restricciones.


Crear ConstraintSet Programáticamente

Ahora bien, si deseas evitar crear un nuevo archivo de layout para añadir la animación, entonces es posible crear establecer las restricciones de los views en el objeto ConstraintSet.

Para ello necesitas clonar las restricciones iniciales a partir del método clone() como ya vimos:

private fun createEndConstraints(){
    endConstraints.clone(constraintLayout)
}

Luego usa las constantes que representan las restricciones del ConstraintLayout. Si revisas la documentación de la clase ConstraintSet encontrarás un valor para cada tipo de restricción existente.

Por ejemplo, para las restricciones de posicionamiento relativo. Si quieres alinear el lado superior del icono del barco con con el lado inferior del botón, usas el método connect() así:

connect(
    R.id.iv_car,
    ConstraintSet.TOP,
    R.id.move_button,
    ConstraintSet.BOTTOM
)

El método connect() materializa las restricciones que especifiques junto a la referencia del widget. Si es el padre, entonces usas el valor ConstraintSet.PARENT_ID.

Basado en esta lógica, el código Kotlin que define el frame final será el siguiente:

private fun createEndConstraints() {
    with(endConstraints) {
        clone(constraintLayout)

        constrainWidth(R.id.iv_boat, 88.px)
        constrainHeight(R.id.iv_boat, 88.px)
        constrainWidth(R.id.iv_car, 44.px)
        constrainHeight(R.id.iv_car, 44.px)

        connect(
            R.id.iv_boat,
            ConstraintSet.TOP,
            ConstraintSet.PARENT_ID,
            ConstraintSet.TOP
        )
        connect(
            R.id.iv_boat,
            ConstraintSet.BOTTOM,
            R.id.move_button,
            ConstraintSet.TOP
        )
        connect(
            R.id.iv_car,
            ConstraintSet.TOP,
            R.id.move_button,
            ConstraintSet.BOTTOM
        )
        connect(
            R.id.iv_car,
            ConstraintSet.BOTTOM,
            ConstraintSet.PARENT_ID,
            ConstraintSet.BOTTOM
        )
    }
}

Usamos la función de alcance with() para la invocación de múltiples métodos asociados al objeto endConstraints y así mejorar la legibilidad.

Debido a que el tamaño se intercambia en el frame final, usamos los métodos constraintWidth() y constraintHeight() para asignar las dimensiones. Como reciben el valor en pixeles, usamos una propiedad de extensión llamada px, que nos ayude a pasar de DPs a esta métrica:

val Int.px: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()

Al final terminamos conectando el borde superior del barco con el padre y su borde inferior con el borde superior del botón.

Para el auto, conectamos su borde superior con el del botón y su lado inferior con el del padre.

Con esto logramos desde nuestro código Kotlin representar la creación dinámica de las restricciones para el fotograma final.


Más Contenidos Android

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!