MotionLayout: las animaciones son mejores, menos código


Google continúa mejorando nuestras vidas al lanzar nuevas bibliotecas y API útiles. Entre los cuales estaba el nuevo MotionLayout. Dada la abundancia de animaciones en nuestras aplicaciones, mi colega Cedric Holtz implementó de inmediato la animación más importante de nuestra aplicación, votar en las citas, usando la nueva API, mientras ahorraba una gran cantidad de código. Comparto la traducción de su artículo.

La conferencia Google I / O 2019 terminó recientemente, en la que se anunciaron actualizaciones y las últimas mejoras de nuestro querido SDK. Personalmente, estaba particularmente interesado en la presentación de Nicholas Road y John Hoford sobre la funcionalidad futura de ConstraintLayout. Más específicamente, sobre su expansión en forma de MotionLayout.

Después de la versión beta, quería implementar una animación de citas basada en esta biblioteca.

Primero, definamos los términos:

"MotionLayout es un RestraintLayout que le permite animar diseños entre diferentes estados". - Documentación


Si no ha leído una serie de artículos de Nicholas Road que explican las ideas clave de MotionLayout, le recomiendo leerlo.

Entonces, con la introducción hecha, ahora veamos qué queremos obtener:



Pila de cartas


Mostramos el mapa desplazado


Para comenzar, agregue MotionLayout al directorio de diseño, que hasta ahora contiene solo una tarjeta superior:

<androidx.constraintlayout.motion.widget.MotionLayout android:id="@+id/motionLayout"     xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent"     app:layoutDescription="@xml/scene_swipe"     app:motionDebug="SHOW_ALL">     <FrameLayout         android:id="@+id/topCard"         android:layout_width="0dp"         android:layout_height="0dp" /> </androidx.constraintlayout.motion.widget.MotionLayout> 

Presta atención a esta línea: app: motionDebug = "SHOW_ALL". Nos permite mostrar información de depuración, la trayectoria de los objetos, los estados con el comienzo y el final de la animación, así como el progreso actual. La línea ayuda mucho al depurar, pero no olvides eliminarla antes de enviarla al producto: no hay ningún recordatorio para esto.

Como puede ver, no establecimos ninguna restricción para la vista aquí. Se tomarán de la escena (MotionScene), que ahora definimos.

Comencemos definiendo el estado inicial: una tarjeta se encuentra en el centro de la pantalla, con sangría alrededor.

 <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto">    <ConstraintSet android:id="@+id/rest">        <Constraint            android:id="@id/topCard"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:layout_marginBottom="50dp"            android:layout_marginEnd="50dp"            android:layout_marginStart="50dp"            android:layout_marginTop="50dp"            app:layout_constraintEnd_toEndOf="parent"            app:layout_constraintStart_toStartOf="parent">    </ConstraintSet> </MotionScene> 

Agregue conjuntos de restricciones (ConstraintSet) pases y me gusta. Reflejarán el estado de la tarjeta superior cuando se desplace completamente hacia la izquierda o hacia la derecha. Queremos que el mapa se detenga antes de desaparecer de la pantalla para mostrar una hermosa animación que confirme nuestra decisión.

 <ConstraintSet    android:id="@+id/pass"    app:deriveConstraintsFrom="@+id/rest">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="200dp"        android:layout_marginStart="50dp"        android:layout_marginTop="20dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> <ConstraintSet    android:id="@+id/like"    app:deriveConstraintsFrom="@id/rest">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="50dp"        android:layout_marginStart="200dp"        android:layout_marginTop="20dp"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintWidth_percent="0.7" /> </ConstraintSet> 

Agregue ambos conjuntos de restricciones a la escena anterior. Son casi idénticos, solo se reflejan en ambos lados de la pantalla.

Ahora tenemos tres conjuntos de restricciones: inicio, me gusta y pasar. Definamos la transición entre estos estados.

Para hacer esto, agregue una transición a la izquierda para deslizar, la otra a la derecha para deslizar.

 <Transition    app:constraintSetEnd="@+id/pass"    app:constraintSetStart="@+id/rest"    app:duration="300">    <OnSwipe        app:dragDirection="dragLeft"        app:onTouchUp="autoComplete"        app:touchAnchorId="@id/topCard"        app:touchAnchorSide="left"        app:touchRegionId="@id/topCard" /> </Transition> <Transition    app:constraintSetEnd="@+id/like"    app:constraintSetStart="@+id/rest"    app:duration="300">    <OnSwipe        app:dragDirection="dragRight"        app:onTouchUp="autoComplete"        app:touchAnchorId="@+id/topCard"        app:touchAnchorSide="right"        app:touchRegionId="@id/topCard" /> </Transition> 

Entonces, para la tarjeta superior, configuramos la animación de deslizamiento a la izquierda y lo mismo: espejo para el deslizamiento a la derecha.

Estas propiedades ayudarán a mejorar la interacción con nuestra escena:

  • touchRegionId: dado que agregamos relleno alrededor del mapa, debemos asegurarnos de que el toque se reconozca solo en el área del mapa en sí, y no en todo el MotionLayout. Esto se puede hacer con touchRegionId.
  • onTouchUp: ¿qué pasará con la animación después de lanzar la tarjeta? Debe seguir adelante o volver a su estado inicial, por lo que se aplica autocompletar.

Veamos que pasó:



El mapa se apaga automáticamente de la pantalla.


Ahora trabajaremos en la animación, que comenzará cuando el mapa salga de la pantalla.

Agregamos dos conjuntos RestraintSet más para cada estado final de nuestras animaciones: el mapa deja la pantalla a izquierda y derecha.

En los siguientes ejemplos, mostraré cómo hacer el estado similar, y el estado de aprobación lo repetirá reflejado. Un ejemplo de trabajo se puede ver completamente en el repositorio .

 <ConstraintSet android:id="@+id/offScreenLike">    <Constraint        android:id="@id/topCard"        android:layout_width="0dp"        android:layout_height="match_parent"        android:layout_marginBottom="80dp"        android:layout_marginEnd="50dp"        android:layout_marginTop="20dp"        app:layout_constraintStart_toEndOf="parent"        app:layout_constraintWidth_percent="0.7" />   </ConstraintSet> 

Ahora, como en el ejemplo anterior, debe determinar la transición del estado de deslizamiento al estado final. La transición debería funcionar automáticamente inmediatamente después de la animación del deslizamiento. Esto se puede hacer usando autoTransition:

 <Transition app:autoTransition="animateToEnd" app:constraintSetEnd="@+id/offScreenLike" app:constraintSetStart="@+id/like" app:duration="150" /> 

¡Ahora tenemos una tarjeta magnética que se puede deslizar desde la pantalla!



Animación del mapa inferior



Ahora hagamos la carta inferior para crear la ilusión de infinito de la baraja.

Agregue un mapa más al diseño, similar al primero:

 <FrameLayout    android:id="@+id/bottomCard"    android:layout_width="0dp"    android:layout_height="0dp"    android:background="@color/colorAccent" /> 

Cambie el XML para establecer las restricciones que se aplican a este mapa en cada etapa de la animación:

 <ConstraintSet android:id="@id/rest">    <!-- ... -->    <Constraint android:id="@id/bottomCard">        <Layout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:layout_marginBottom="50dp"            android:layout_marginEnd="50dp"            android:layout_marginStart="50dp"            android:layout_marginTop="50dp" />        <Transform            android:scaleX="0.90"            android:scaleY="0.90" />    </Constraint> </ConstraintSet> <ConstraintSet    android:id="@+id/offScreenLike"    app:deriveConstraintsFrom="@id/like">    <!-- ... -->    <Constraint android:id="@id/bottomCard">        <Transform            android:scaleX="1"            android:scaleY="1" />    </Constraint> </ConstraintSet> 

Para hacer esto, podemos usar la conveniente propiedad ConstraintSet.

Por defecto, cada nuevo conjunto toma atributos del MotionLayout primario. Pero usando el indicador deriveConstraintsFrom, puede establecer otro padre para nuestro conjunto. Debe tenerse en cuenta que si establecemos restricciones utilizando la etiqueta de restricción, redefinimos todas las restricciones del conjunto primario. Para evitar esto, puede establecer atributos específicos en las etiquetas para que solo se reemplacen.



En nuestro caso, esto significa que en el conjunto de pases no definimos la etiqueta de diseño, sino que la copiamos del padre. Sin embargo, anulamos Transformar, por lo tanto, reemplazamos todos los atributos especificados en la etiqueta Transformar con el nuestro, en este caso, un cambio de escala.

Así de fácil es usar MotionLayout para agregar un nuevo elemento e integrarlo sin problemas con las animaciones de nuestra escena.



Hacer que la animación sea interminable



Una vez completada la animación, la carta superior no se puede deslizar, porque ahora se ha convertido en una carta inferior. Para obtener una animación sin fin, debes intercambiar cartas.

Al principio quería hacer esto con una nueva transición:

 <Transition    app:autoTransition="jumpToEnd"    app:constraintSetEnd="@+id/rest"    app:constraintSetStart="@+id/offScreenLike"    app:duration="0" /> 



Toda la animación se reproduce como debería. ¡Ahora tenemos una pila de cartas que puedes deslizar sin parar!

Al dar un pequeño golpe, noté algo. La transición al final de la animación del mazo se detiene cuando tocas la carta. Aunque la duración de la animación es cero, todavía se detiene, lo cual es malo.



Logré ganar de una sola manera: cambiando programáticamente la transición activa en MotionLayout.

Para hacer esto, estableceremos una devolución de llamada al finalizar la animación. Tan pronto como se completan offScreenLike y offScreenPass, simplemente restablecemos la transición al estado de reposo y ponemos a cero el progreso.

 motionLayout.setTransitionListener(object : TransitionAdapter() {    override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) {        when (currentId) {            R.id.offScreenPass,            R.id.offScreenLike -> {                motionLayout.progress = 0f                motionLayout.setTransition(R.id.rest, R.id.like)            }        }    }   }) 

No importa qué transición establezcamos, pasemos o nos guste, cuando deslizamos, cambiamos a la deseada.



Se ve igual, ¡pero la animación no se detiene! ¡Sigamos adelante!

Enlace de datos


Crear datos de prueba para mostrar en los mapas. Por ahora, nos limitaremos a cambiar el color de fondo de cada tarjeta.

Creamos un ViewModel con un método svayp que solo sustituye datos nuevos. Añádelo a la Actividad de esta manera:

 val viewModel = ViewModelProviders .of(this) .get(SwipeRightViewModel::class.java) viewModel .modelStream .observe(this, Observer { bindCard(it) }) motionLayout.setTransitionListener(object : TransitionAdapter() { override fun onTransitionCompleted(motionLayout: MotionLayout, currentId: Int) { when (currentId) { R.id.offScreenPass, R.id.offScreenLike -> { motionLayout.progress = 0f motionLayout.setTransition(R.id.rest, R.id.like) viewModel.swipe() } } } }) 

Queda por informar a ViewModel sobre la finalización de la animación de deslizamiento, y actualizará los datos que se muestran actualmente.



Iconos emergentes


Agregue dos vistas, que al deslizar aparecen en uno de los lados de la pantalla (solo se muestra una a continuación, la segunda se refleja).

 <ImageView android:id="@+id/likeIndicator" android:layout_width="0dp" android:layout_height="0dp" /> 

Ahora para los mapas, debe establecer los estados de animación con estas vistas.

 <ConstraintSet android:id="@id/rest">    <!-- ... -->    <Constraint android:id="@+id/like">        <Layout            android:layout_width="40dp"            android:layout_height="40dp"            app:layout_constraintBottom_toBottomOf="parent"            app:layout_constraintStart_toEndOf="parent"            app:layout_constraintTop_toTopOf="parent" />        <Transform            android:scaleX="0.5"            android:scaleY="0.5" />        <PropertySet android:alpha="0" />    </Constraint>   </ConstraintSet> <ConstraintSet    android:id="@+id/like"    app:deriveConstraintsFrom="@id/rest">    <!-- ... -->    <Constraint android:id="@+id/like">        <Layout            android:layout_width="100dp"            android:layout_height="100dp"            app:layout_constraintBottom_toBottomOf="@id/topCard"            app:layout_constraintEnd_toEndOf="@id/topCard"            app:layout_constraintStart_toStartOf="@id/topCard"            app:layout_constraintTop_toTopOf="@id/topCard" />        <Transform            android:scaleX="1"            android:scaleY="1" />        <PropertySet android:alpha="1" />    </Constraint> </ConstraintSet> 

No es necesario establecer restricciones en las animaciones que van más allá de la pantalla, ya que se heredan de los padres. Y en nuestro caso, este es un estado de deslizamiento.

Eso es todo lo que necesitamos hacer. Ahora puede agregar componentes a las cadenas de animación con mucha facilidad.



Ejecute la animación mediante programación.



Podemos hacer dos botones en las tarjetas para que el usuario no solo pueda deslizar, sino controlar el uso de los botones.

Cada botón inicia la misma animación que el deslizamiento.

Como de costumbre, suscríbase a los clics de los botones e inicie la animación directamente en el objeto MotionLayout:

 likeButton.setOnClickListener {    motionLayout.transitionToState(R.id.like) } passButton.setOnClickListener {    motionLayout.transitionToState(R.id.pass) } 

Necesitamos agregar botones a las tarjetas superior e inferior para que la animación se reproduzca continuamente. Sin embargo, para el mapa inferior, no es necesaria una suscripción de clic, ya que no es visible o el mapa superior está animado, y no queremos interrumpirlo.



Otro gran ejemplo de cómo MotionLayout maneja los cambios de estado para nosotros. Reduzcamos la velocidad de la animación ligeramente:



Mire la transición que MotionLayout realiza cuando el pase reemplaza como. La magia!

Desliza el mapa a lo largo de la curva


Supongamos que nos gusta si el mapa no se mueve en línea recta, sino en una curva (para ser sincero, solo quería intentar hacer eso).

Luego debe definir KeyPosition para el movimiento en ambas direcciones, de modo que la trayectoria del movimiento esté curvada por un arco.

Agregue esto a la escena de movimiento:

 <Transition    app:constraintSetEnd="@+id/like"    app:constraintSetStart="@+id/rest"    app:duration="300">    <!-- ... -->    <KeyFrameSet>        <KeyPosition            app:drawPath="path"            app:framePosition="50"            app:keyPositionType="pathRelative"            app:motionTarget="@id/topCard"            app:percentX="0.5"            app:percentY="-0.1" />           </KeyFrameSet> </Transition> 


Ahora el mapa se mueve a lo largo de un camino curvo no banal. Mágicamente!

Conclusión


Cuando compara la cantidad de código que obtuve al crear estas animaciones con nuestra implementación actual de animaciones similares en producción, el resultado es asombroso.

MotionLayout maneja imperceptiblemente las transiciones de cancelación (por ejemplo, cuando se toca), crea cadenas de animación, cambia las propiedades durante las transiciones y mucho más. Esta herramienta cambia fundamentalmente todo, simplificando en gran medida la lógica de la interfaz de usuario.

Hay algunas cosas más en las que vale la pena trabajar (principalmente deshabilitar animaciones y desplazamiento bidireccional en RecyclerView), pero estoy seguro de que esto es solucionable.

Recuerde que la biblioteca todavía está en estado beta, pero ya nos abre muchas oportunidades emocionantes. Esperamos con ansias el lanzamiento de MotionLayout, que, estoy seguro, será útil más de una vez en el futuro. Puede ver la aplicación completamente funcional de este artículo en el repositorio .

PD: y dado que me dieron la palabra como traductor, nuestro equipo de Android tiene un lugar para un desarrollador . Gracias por su atencion

Source: https://habr.com/ru/post/458854/


All Articles