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.
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
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.
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:
- Crear una instancia de
ConstraintSet
con su constructor público - Cargar las restricciones con su método
load()
oclone()
- Aplicar las restricciones del conjunto al
ConstraintLayout
conapplyTo()
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
- Objetos virtuales de ayuda del ConstraintLayout
- Tutoriales de interfaz gráfica en Android (todo)
- Cursos de desarrollo 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!