Novedades de la versión 2.0 de ConstraintLayout

La nueva versión de ConstraintLayout ya ha hecho acto de presencia y viene cargada de nuevas funcionalidades de diseño, múltiples correcciones de errores y una nueva clase para hacer más ameno eso de hacer animaciones: MotionLayout.

En este post nos vamos a centrar en tres funcionalidades nuevas: Flow, Layer y la ya nombrada MotionLayout. Si te interesa saber qué otras novedades contiene esta versión o qué errores
se han solucionado, puedes consultarlo aquí.

Antes de empezar, agrega la dependencia de ConstraintLayout 2.0 en el archivo build.gradle de tu app o módulo:

dependencies {
    implementation "androidx.constraintlayout:constraintlayout:2.0.0"
}

 

 

Flow

Flow te permite mostrar una cadena de vistas en varias líneas o secciones (horizontal o verticalmente). Resulta muy útil cuando tienes que colocar vistas en cadena pero no estás seguro cuan
grande va a ser el contenedor en tiempo de ejecución. Es una especie de LinearLayout pero más flexible en cuanto a organización de elementos y sin ser un contenedor (ViewGroup) como tal. Esto te permitirá definir los márgenes o espacios de forma general sin tener que ponerlos individualmente en cada elemento.

Para empezar, añade a tu layout una vista de tipo <androidx.(...).widget.Flow> independientemente de las vistas que van a formar parte de la cadena (recuerda que no es un ViewGroup), aquí puedes ver un ejemplo:

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/flow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="card0, card1, card2, card3"
        app:flow_maxElementsWrap="3"
        app:flow_verticalAlign="top"
        app:flow_verticalGap="12dp"
        app:flow_horizontalGap="12dp"
        app:flow_wrapMode="chain"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <FrameLayout
        android:id="@+id/card0"
        android:layout_width="120dp"
        android:layout_height="80dp"
        android:background="@color/colorPrimaryDark" />

    <FrameLayout
        android:id="@+id/card1"
        android:layout_width="120dp"
        android:layout_height="80dp"
        android:background="@color/blue" />

    <FrameLayout
        android:id="@+id/card2"
        android:layout_width="120dp"
        android:layout_height="80dp"
        android:background="@color/colorAccent" />

    <FrameLayout
        android:id="@+id/card3"
        android:layout_width="120dp"
        android:layout_height="80dp"
        android:background="@color/colorPrimary" />

 

Funciones relevantes de Flow que nos pueden servir:

  • constraint_referenced_ids (setReferencedIds(int[] ids)). Declara, de modo secuencial y separados por comas, los ids de las vistas que componen esa estructura.
  • flow_wrapMode (setWrapMode(int mode)). Establece el modo de ajuste para el diseño, las opciones son:
    • none. Valor por defecto, crea una sola cadena y si los elementos no caben no hará un salto de línea.
    • chain. Si no hay suficiente espacio para los elementos referenciados, creará cadenas adicionales después de la primera.
    • aligned. Si no hay suficiente espacio para los elementos referenciados, envolverá los elementos alineados como si fuera una tabla.

"flow_wrapMode en ConstraintLayout 2.0"

  • flow_horizontalAlign (setHorizontalAlign(int align)). Configura la alineación horizontal de los elementos en el diseño, las opciones son: start, end y center.
  • flow_verticalAlign (setVerticalAlign(int align)). Configura la alineación vertical de los elementos en el diseño, las opciones son: top, baseline, center y bottom.
  • flow_verticalGap (setVerticalGap(int gap)): Configurar el margen vertical entre elementos.
  • flow_horizontalGap (setHorizontalGap(int gap)): Configurar el margen horizontal entre elementos.
  • flow_maxElementsWrap (setMaxElementsWrap (int max)): Configura el número máximo de elementos envueltos. Por ejemplo, si tienes 4 elementos “envueltos” pero tienes un número máximo de 3, mostrará 3 elementos alineados y uno suelto en otra posición.
"flow_wrapMode en ConstraintLayout 2.0"

 

Esto es solo un resumen de lo que te puede aportar Flow, si quieres saber más, acude a la documentación oficial de Android Developers.

 

 

Layer

Layer sirve para crear una capa virtual de varias vistas. La diferencia con Flow es que esta no sirve para presentar las propias vistas, sirve simplemente para aplicar transformaciones o animaciones de forma simultánea. Es muy útil para casos en los que queremos hacer rotate, translate, scale o visibility de varias vistas a la vez.

Para usar Layer, añade a tu layout una vista de tipo <androidx.(...).widget.Layer> independientemente de las vistas que van a formar parte de la capa, tal y como lo usamos con Flow, aquí podemos ver un ejemplo en el cual mostramos y ocultamos dos vistas usando Layer y Motion:

	<TextView
        android:id="@+id/motion_label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
       	android:layout_margin="16dp"
        android:text="Text view 1 mostrado usando layer"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:textColor="@android:color/black"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/motion_label_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/dummy_text"
        android:padding="16dp"
        app:layout_constraintTop_toBottomOf="@+id/motion_label"
        app:layout_constraintStart_toStartOf="parent"
        android:textColor="@android:color/black"
        app:layout_constraintEnd_toEndOf="parent" />

    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/motion__container__layer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:constraint_referenced_ids="motion_label, motion_label_1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

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

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="250">
        <OnClick
            motion:clickAction="toggle"
            motion:targetId="@+id/motion__btn__show" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/motion__container__layer"
            motion:visibilityMode="ignore">
            <CustomAttribute motion:attributeName="visibility" motion:customIntegerValue="8"/>
        </Constraint>
        <Constraint android:id="@id/motion__btn__show">
            <CustomAttribute motion:attributeName="text" motion:customStringValue="Mostrar"/>
        </Constraint>
    </ConstraintSet>

    <ConstraintSet
        android:id="@+id/end"
        motion:deriveConstraintsFrom="@id/start">
        <Constraint android:id="@id/motion__container__layer"
            motion:visibilityMode="ignore">
            <CustomAttribute motion:attributeName="visibility" motion:customIntegerValue="0"/>
        </Constraint>
        <Constraint android:id="@id/motion__btn__show">
            <CustomAttribute motion:attributeName="text" motion:customStringValue="Ocultar"/>
        </Constraint>
    </ConstraintSet>

</MotionScene>
"Text view con Layer en ConstraintLayout 2.0"

 

 

MotionLayout

MotionLayout es el plato fuerte de esta versión, una clase que hereda de ConstraintLayout y proporciona un sistema para coordinar animaciones de múltiples vistas, de una forma más sencilla y limpia.

Mediante una serie de restricciones (aquí llamadas ConstraintSet) puedes personalizar la forma en que las vistas se mueven o transforman, controlar la velocidad de las animaciones y manejar gestos (OnSwipe, OnClick,etc.).

 

Porqué usar MotionLayout

MotionLayout se creó para cerrar la brecha entre las transiciones y el manejo de movimientos complejos. Es una mezcla entre las clases TransitionManager y CoordinatorLayout, dejándote describir transiciones como la primera (pero pudiendo animar propiedades de las vistas) y soportando el uso de gestos como la segunda.

Además, sus animaciones pueden ser completamente declaradas en XML, sin ser necesario escribir una línea de código en Java o Kotlin. Si eso de escribir en XML tampoco te va, Android Studio te proporciona una herramienta gráfica para montar estas animaciones. Pero no todo es color de rosas, MotionLayout tiene la limitación de que solo funciona para sus hijos directos, a diferencia de TransitionManager que funciona con jerarquías anidadas. A pesar de esto, MotionLayout parece la mejor opción cuando se necesita mover, cambiar el tamaño o animar los elementos de la interfaz de usuario con los que el propio usuario necesita interactuar.

 

¿Cómo usar MotionLayout?

Básicamente se usa como una <ConstraintLayout> con la diferencia de que la descripción de restricciones no se añaden en el mismo XML, sino que se separa esta lógica en otro XML llamado escena (MotionScene). De esta forma, el layout solo contiene las vistas y las propiedades de las mismas. Este archivo XML contendrá todo lo necesario para aplicar la animación (ConstraintSets, transiciones, KeyFrames, control de gestos...etc).

  • Layout:
    ​
    <androidx.constraintlayout.motion.widget.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/motion_simple_movement_scene">
    
        <View
            android:id="@+id/motion__view__item"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:background="@color/colorPrimary"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </androidx.constraintlayout.motion.widget.MotionLayout>

     

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

    Cada escena tiene una serie de <ConstraintSet> que se encargan de organizar y guardar un conjunto de restricciones <Constraint> para utilizar en una vista, cada <Constraint> tiene un id que tiene que coincidir con el id de la vista a la que hace referencia. Una restricción puede ser desde el posicionamiento de la vista en la pantalla hasta el color de fondo de la propia vista. En el siguiente ejemplo podemos ver como con el uso de la etiqueta <Layout> definimos el tamaño de la vista y su posicionamiento, mientras que con la etiqueta <CustomAttribute> podemos definir el color de fondo de la vista:
     

    <MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:motion="http://schemas.android.com/apk/res-auto">
    
        <ConstraintSet android:id="@+id/start">
            <Constraint android:id="@id/motion__view__item">
                <Layout
                    android:layout_width="64dp"
                    android:layout_height="64dp"
                    android:layout_marginStart="8dp"
                    motion:layout_constraintBottom_toBottomOf="parent"
                    motion:layout_constraintStart_toStartOf="parent"
                    motion:layout_constraintTop_toTopOf="parent" />
    
                <CustomAttribute motion:attributeName="BackgroundColor"
                    motion:customColorValue="@color/colorPrimary"/>
    
            </Constraint>
        </ConstraintSet>
    
    </MotionScene>

     

  • <CustomAttribute> contiene dos atributos propios:
    • attributeName. Es obligatorio y debe coincidir con un objeto que tenga métodos get y set. Por ejemplo, backgroundColor es compatible, ya que nuestra vista tiene métodos getBackgroundColor() y setBackgroundColor().
    • El otro atributo que debes proporcionar se basa en el tipo de valor. Los tipos son: customColorValue, customIntegerValue, customFloatValue, customStringValue, customDimension y customBoolean.
       

Para crear una animación, es necesario definir una etiqueta de tipo <Transition>, en esta se indica la ConstraintSet que usa al inicio de la transición y cuál usa al final. En el siguiente ejemplo se puede observar la creación de una transición donde la vista empieza en la izquierda y acaba en la derecha de la pantalla.

Sus tres atributos son:

  • constraintSetStart. Estado inicial de la secuencia de movimiento. Puede ser el ID de un <ConstraintSet> o un diseño. Para especificar un <ConstraintSet>, establece este atributo en "@+id/constraintSetId". Para especificar un diseño, puedes especificarlo como “@layout/layoutState”.
  • constraintSetEnd. Estado final de la secuencia de movimiento. Puede ser el ID de un <ConstraintSet> o un diseño. Para especificar un <ConstraintSet>, establece este atributo en "@+id/constraintSetId". Para especificar un diseño, puedes especificarlo como “@layout/layoutState”.
  • duration. Duración de la secuencia en movimiento, expresada en milisegundos.
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="1000">
        <OnSwipe
            motion:dragDirection="dragRight"
            motion:touchAnchorId="@id/motion__view__item"
            motion:touchAnchorSide="right" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/motion__view__item">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginStart="8dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintStart_toStartOf="parent"
                motion:layout_constraintTop_toTopOf="parent" />
            <CustomAttribute motion:attributeName="BackgroundColor"
                motion:customColorValue="@color/colorPrimary"/>
        </Constraint>
    </ConstraintSet>

    <ConstraintSet
        android:id="@+id/end"
        motion:deriveConstraintsFrom="@id/start">

        <Constraint android:id="@id/motion__view__item">
            <Layout
                android:layout_width="64dp"
                android:layout_height="64dp"
                android:layout_marginEnd="8dp"
                motion:layout_constraintEnd_toEndOf="parent" />
            <CustomAttribute motion:attributeName="BackgroundColor"
                motion:customColorValue="@color/colorAccent"/>
        </Constraint>
    </ConstraintSet>

</MotionScene>

Con la etiqueta <OnSwipe> o <OnClick> podemos definir que la animación se ejecute con el gesto que indiquemos. Estas etiquetas de gesto contiene tres atributos muy importantes:

  • touchAnchorId . El id de la vista a la que le quieres añadir el gesto.
  • touchAnchorSide. Para los OnSwipe, lado de la vista de destino al que está anclado el deslizamiento. MotionLayout intentará mantener una distancia constante entre ese anclaje y el dedo del usuario. Los valores aceptables son left, right, top y bottom.
  • dragDirection. Para los OnSwipe, la dirección en la que se debe deslizar.
"Ejemplo de animación simple con MotionLayout en ConstraintLayout 2.0"

 

Animaciones avanzadas

Para animaciones más complejas, donde no solo exista una transformación de la vista al inicio y al final de la secuencia, debemos usar la etiqueta <KeyFrameSet>. Esta etiqueta contiene los nodos <KeyPosition> o <KeyAttribute>, cada uno de esos nodos especifica la posición o atributos de una vista en un punto específico del movimiento. MotionLayout anima suavemente la vista desde el punto de partida hasta cada uno de esos puntos intermedios y, luego, hasta el destino final.

Los atributos de <KeyAttribute> son:

  • motionTarget. Id de la vista a la que queremos hacer referencia.
  • framePosition. Número entero del 1 al 99 que especifica en qué momento de la secuencia de movimiento la vista alcanza el punto especificado por <KeyAttribute>. Por ejemplo, si framePosition es 25, la vista alcanza el punto especificado cuando se completa una cuarta parte del movimiento.
  • Algunos de los atributos que puedes cambiar son: visibility, alpha, elevation y rotation. Puedes consultar todas las posibilidades en la documentación oficial de KeyAttribute.

Los atributos de <KeyPosition> son:

  • motionTarget. Id de la vista a la que queremos hacer referencia.
  • framePosition. Número entero del 1 al 99 que especifica en qué momento de la secuencia de movimiento la vista alcanza el punto especificado por <KeyPosition>. Por ejemplo, si framePosition es 25, la vista alcanza el punto especificado cuando se completa una cuarta parte del movimiento.
  • percentX o percentY. Especifica la posición a la que debe llegar la vista.
  • keyPositionType. Especifica cómo se interpretan los valores percentX y percentY. Sus valores son:
    • parentRelative: percentX y percentY se especifican en relación con la vista superior. X es el eje horizontal, que va de 0 (el lado izquierdo) a 1 (el lado derecho). Y es el eje vertical, donde 0 es la parte superior y 1 es la parte inferior. Por ejemplo, si quieres que la vista de destino llegue a un punto en la mitad del lado derecho de la vista superior, debes establecer percentX como 1 y percentY como 0.5.
    • deltaRelative: percentX y percentY se especifican en relación con la distancia que recorre la vista en toda la secuencia de movimiento. X es el eje horizontal e Y es el eje vertical. En ambos casos, 0 es la posición inicial de la vista en ese eje y 1 es la posición final. Por ejemplo, supongamos que la vista de destino se mueve 100dp hacia arriba y 100dp hacia la derecha, pero quieres que la vista empiece moviéndose hacia abajo 40dp durante el primer cuarto del movimiento y luego que vuelva hacia arriba. Para ello, configura framePosition como 25, keyPositionType como deltaRelative y percentY como -0.4.
    • pathRelative: el eje X es la dirección en la que se mueve la vista de destino a lo largo de la ruta, donde 0 es la posición inicial y 1 es la posición final. El eje Y es perpendicular al eje X. Los valores positivos están a la izquierda y los valores negativos están a la derecha de la ruta. Si configuras un porcentaje Y no nulo, se curvará la vista en una dirección o en otra. Por lo tanto, la posición inicial de la vista es (0,0) y la posición final es (1,0). Por ejemplo, supongamos que quieres que la vista use la mitad de la secuencia de movimiento hasta abarcar el 10% de la distancia total y luego acelere hasta cubrir el otro 90%. Para ello, configura framePosition en 50, keyPositionType como pathRelative y percentX como 0.1.
<KeyFrameSet>
    <KeyAttribute
        android:rotation="-45"
        android:scaleX="2"
        android:scaleY="2"
        motion:framePosition="50"
        motion:motionTarget="@id/motion__movement__keyframe" />
    <KeyPosition
        motion:framePosition="50"
        motion:keyPositionType="pathRelative"
        motion:motionTarget="@id/motion__movement__keyframe"
        motion:percentY="-0.3" />
</KeyFrameSet>
"Ejemplo de keyframe con MotionLayout en ConstraintLayout 2.0"

 

Para casos donde nuestra animación deba realizar transformaciones de forma repetitiva, MotionLayout nos proporciona la etiqueta <KeyCycle>. Con esta etiqueta, la línea de tiempo de la animación se divide en subsecciones (ciclos), las cuáles contienen una o más ondas que definen cómo se modificará la vista a lo largo del tiempo.

Los atributos necesarios para definir un ciclo son:

  • motionTarget. Id de la vista a la que hace referencia.
  • framePosition. Posición en la línea del tiempo en el que debe comenzar el ciclo.
  • wavePeriod. El número de ondas que se incluirán en el ciclo.
  • waveOffset. Desplaza el ciclo en la cantidad especificada de la línea de base del fotograma clave.
  • waveShape. Forma de la onda (sin, cos, sawtooth, square, triangle, bounce o reverseSawtooth).
  • Los atributos de la vista que puedes modificar son los mismos de los que ya hablamos en el apartado de <KeyFrame>.
<KeyFrameSet>
    <KeyCycle
        android:translationY="50dp"
        motion:framePosition="100"
        motion:motionTarget="@id/motion__movement__keyframe"
        motion:waveOffset="0"
        motion:wavePeriod="0"
        motion:waveShape="sin" />
    <KeyCycle
        android:translationY="50dp"
        motion:framePosition="50"
        motion:motionTarget="@id/motion__movement__keyframe"
        motion:waveOffset="0"
        motion:wavePeriod="1"
        motion:waveShape="sin" />
    <KeyCycle
        android:translationY="50dp"
        motion:framePosition="0"
        motion:motionTarget="@id/motion__movement__keyframe"
        motion:waveOffset="0"
        motion:wavePeriod="0"
        motion:waveShape="sin" />
</KeyFrameSet>

 

 

Ejemplo

A continuación veremos un pequeño ejemplo de uso de Flow, Layer y MotionLayout juntos, un menú de Floating Buttons que cuando haces clic sobre uno aparecen todas las opciones disponibles. Tal que así:

"Ejemplo de floating menu con MotionLayout en ConstraintLayout 2.0"

 

Añadimos en el layout las diferentes vistas: la vista Flow para definir la organización de los botones de acción, la vista Layer para declarar un conjunto de vistas las cuales mostramos u ocultamos y MotionLayout como contenedor padre del layout.

<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/motion_example_scene"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.helper.widget.Flow
        android:id="@+id/motion__flow__actions"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        app:flow_wrapMode="aligned"
        app:flow_horizontalGap="12dp"
        app:constraint_referenced_ids="motion__btn__add, motion__btn__delete, motion__btn__update"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/motion__btn__menu"
        tools:ignore="MissingConstraints" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/motion__btn__add"
        android:layout_width="wrap_content"
        android:contentDescription="Crear"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_input_add"
        android:tint="@android:color/white" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/motion__btn__delete"
        android:layout_width="wrap_content"
        android:contentDescription="Borrar"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_menu_delete"
        android:tint="@android:color/white" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/motion__btn__update"
        android:layout_width="wrap_content"
        android:contentDescription="Editar"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:src="@android:drawable/ic_menu_edit"
        android:tint="@android:color/white" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/motion__btn__menu"
        android:layout_width="wrap_content"
        android:contentDescription="Menú"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        app:elevation="0dp"
        android:tint="@android:color/white"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <androidx.constraintlayout.helper.widget.Layer
        android:id="@+id/motion_example__layer__actions"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:constraint_referenced_ids="motion__btn__add, motion__btn__update, motion__btn__delete"/>

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/motion__img__menu"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:tint="@android:color/white"
        android:clickable="false"
        app:tint="@android:color/white"
        android:src="@android:drawable/ic_menu_more"
        app:altSrc="@android:drawable/ic_delete"
        app:layout_constraintEnd_toEndOf="@+id/motion__btn__menu"
        app:layout_constraintBottom_toBottomOf="@+id/motion__btn__menu"
        app:layout_constraintTop_toTopOf="@+id/motion__btn__menu"
        app:layout_constraintStart_toStartOf="@+id/motion__btn__menu"/>

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

 

Nota: Hay una vista de tipo ImageFilterView delante de nuestro FloatingButton la cual usaremos para mostrar el icono de este botón, para que al pulsarlo éste haga una animación, y no sea un cambio de imagen normal y corriente.

En la escena añadiremos una Transition de tipo OnClick cuyo id objetivo sea nuestro FloatingButton principal (el único que se muestra al principio).

<Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="300">
        <OnClick
            motion:targetId="@+id/motion__btn__menu" />
    </Transition>

 

Añadimos una ConstraintSet de inicio que ocultará nuestra vista Layer, anteriormente definida, esto hará que los botones que pertenecen a esa capa se mantengan ocultos al inicio, hay que definir también las constraints de posicionamiento de la vista. Por último, definiremos el atributo "crossfade" de ImageFilterView a 0 para indicar que queremos mostrar el primer icono de la vista.

<ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/motion_example__layer__actions"
            motion:visibilityMode="ignore">
            <Layout android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:layout_marginBottom="16dp"
                android:layout_marginEnd="16dp"
                motion:layout_constraintBottom_toBottomOf="parent"
                motion:layout_constraintEnd_toStartOf="@+id/motion__btn__menu"/>
            <CustomAttribute motion:attributeName="visibility" motion:customIntegerValue="8"/>
        </Constraint>
        <Constraint android:id="@id/motion__img__menu">
            <Layout android:layout_height="24dp"
                android:layout_width="24dp"
                motion:layout_constraintStart_toStartOf="@+id/motion__btn__menu"
                motion:layout_constraintEnd_toEndOf="@+id/motion__btn__menu"
                motion:layout_constraintTop_toTopOf="@+id/motion__btn__menu"
                motion:layout_constraintBottom_toBottomOf="@+id/motion__btn__menu"/>
            <CustomAttribute
                motion:attributeName="crossfade"
                motion:customFloatValue="0" />
        </Constraint>
    </ConstraintSet>

 

Para la ConstraintSet de fin, haremos exactamente lo mismo pero mostrando el Layer, en vez de ocultarlo, y cambiando el atributo "crossfade" de nuestro ImageFilterView a 1, para que haga la animación de cambiar al segundo icono.

Nota: Lo ideal sería definir que los botones de acción aparecieran uno a uno desde detrás del botón principal, para que así quedara mejor, así que no recomendaría el uso de Flow para este caso.

Posts relacionados

Todas las novedades presentadas en la Google I/O 2021

Todo sobre la Google I/O 2021

Integración de Hilt en un proyecto Android multimódulo

Cómo integrar Hilt en un proyecto Android modular

Comentarios
¿Qué opinas? Escríbenos. Nos encantará leerte :)