Cómo animar movimiento con física de resortes

Prueba el estilo de Compose
Jetpack Compose es el kit de herramientas de IU recomendado para Android. Aprende a usar animaciones en Compose.

El movimiento basado en la física se impulsa por la fuerza. La fuerza de resorte es una de las fuerzas que guía la interactividad y el movimiento. Una fuerza de resorte tiene las siguientes propiedades: amortiguamiento y rigidez. En una animación basada en resortes, el valor y la velocidad se calculan en función de la fuerza de resorte que se aplican en cada fotograma.

Si quieres que las animaciones de tu app se ralenticen en una sola dirección, procura usar una animación de desplazamiento basada en la fricción.

Ciclo de vida de una animación de resorte

En una animación basada en resortes, la clase SpringForce te permite personalizar la rigidez del resorte, su proporción de amortiguamiento y su posición final. En cuanto comienza la animación, la fuerza de resorte actualiza el valor de la animación y la velocidad en cada fotograma. La animación continúa hasta que la fuerza de resorte alcanza un equilibrio.

Por ejemplo, si arrastras el ícono de una app por la pantalla y lo sueltas levantando el dedo del ícono, el ícono regresará a su lugar original mediante una fuerza invisible pero conocida.

En la figura 1, se muestra un efecto de resorte similar. El signo más (+) en el medio del círculo indica la fuerza aplicada mediante un gesto táctil.

Liberación del resorte
Figura 1: Efecto de liberación del resorte

Cómo crear una animación de resorte

Los pasos generales a fin de compilar una animación de resorte para tu aplicación son los siguientes:

En las siguientes secciones, se analizan en detalle los pasos generales para compilar una animación de resorte.

Cómo agregar la biblioteca de compatibilidad

Para usar la biblioteca basada en la física, debes agregarla a tu proyecto de la siguiente manera:

  1. Abre el archivo build.gradle del módulo de tu app.
  2. Agrega la biblioteca de compatibilidad a la sección dependencies.

    Groovy

            dependencies {
                def dynamicanimation_version = '1.0.0'
                implementation "androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version"
            }
            

    Kotlin

            dependencies {
                val dynamicanimation_version = "1.0.0"
                implementation("androidx.dynamicanimation:dynamicanimation:$dynamicanimation_version")
            }
            

    Para ver las versiones actuales de esta biblioteca, consulta la información sobre Dynamicanimation en la página de versiones.

Cómo crear una animación de resorte

La clase SpringAnimation te permite crear una animación de resorte para un objeto. Para compilar una animación de resorte, debes crear una instancia de la clase SpringAnimation y proporcionar un objeto, la propiedad del objeto que quieras animar y una posición final opcional del resorte donde quieres que descanse la animación.

Nota: En el momento de crear una animación de resorte, la posición final de este es opcional. Sin embargo, debe definirse antes de iniciar la animación.

Kotlin

val springAnim = findViewById<View>(R.id.imageView).let { img ->
    // Setting up a spring animation to animate the view’s translationY property with the final
    // spring position at 0.
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0f)
}

Java

final View img = findViewById(R.id.imageView);
// Setting up a spring animation to animate the view’s translationY property with the final
// spring position at 0.
final SpringAnimation springAnim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y, 0);

La animación basada en resortes puede animar vistas en la pantalla cambiando las propiedades reales en los objetos de vista. Las siguientes vistas están disponibles en el sistema:

  • ALPHA: Representa la transparencia alfa de la vista. El valor es 1 (opaco) de forma predeterminada, y el valor 0 representa la transparencia total (no visible).
  • TRANSLATION_X, TRANSLATION_Y y TRANSLATION_Z: Estas propiedades controlan la ubicación de la vista como una delta a partir de las coordenadas izquierda y superior, y la elevación, que establece su contenedor de diseño.
  • ROTATION, ROTATION_X y ROTATION_Y: Estas propiedades controlan la rotación en 2D (propiedad rotation) y 3D alrededor del punto de pivote.
  • SCROLL_X y SCROLL_Y: Estas propiedades indican el desplazamiento de los bordes izquierdo y superior de origen en píxeles. También indica la posición en términos de cuánto se desplaza la página.
  • SCALE_X y SCALE_Y: Estas propiedades controlan el escalamiento en 2D de una vista alrededor de su punto de pivote.
  • X, Y y Z: Estas son propiedades de utilidad básicas para describir la ubicación final de la vista en su contenedor.

Cómo registrar objetos de escucha

La clase DynamicAnimation proporciona dos objetos de escucha: OnAnimationUpdateListener y OnAnimationEndListener. Estos objetos de escucha escuchan las actualizaciones en la animación, como cuando hay un cambio en el valor de la animación y cuando esta llega a su fin.

OnAnimationUpdateListener

Si deseas animar varias vistas para crear una animación encadenada, puedes configurar OnAnimationUpdateListener para recibir una devolución de llamada cada vez que haya un cambio en la propiedad de la vista actual. La devolución de llamada notifica a la otra vista para que actualice su posición del resorte en función del cambio generado en la propiedad de la vista actual. Para registrar el objeto de escucha, sigue estos pasos:

  1. Llama al método addUpdateListener() y adjunta el objeto de escucha a la animación.

    Nota: Debes registrar el objeto de escucha de actualizaciones antes de que comience la animación. Sin embargo, el objeto de escucha de actualizaciones debe registrarse solo si necesitas una actualización por fotograma sobre los cambios de valores de la animación. Un objeto de escucha de actualizaciones evita que la animación se ejecute en un subproceso separado.

  2. Anula el método onAnimationUpdate() para notificar al llamador sobre el cambio en el objeto actual. En el siguiente código de muestra, se ilustra el uso general de OnAnimationUpdateListener.

Kotlin

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
val (anim1X, anim1Y) = findViewById<View>(R.id.view1).let { view1 ->
    SpringAnimation(view1, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view1, DynamicAnimation.TRANSLATION_Y)
}
val (anim2X, anim2Y) = findViewById<View>(R.id.view2).let { view2 ->
    SpringAnimation(view2, DynamicAnimation.TRANSLATION_X) to
            SpringAnimation(view2, DynamicAnimation.TRANSLATION_Y)
}

// Registering the update listener
anim1X.addUpdateListener { _, value, _ ->
    // Overriding the method to notify view2 about the change in the view1’s property.
    anim2X.animateToFinalPosition(value)
}

anim1Y.addUpdateListener { _, value, _ -> anim2Y.animateToFinalPosition(value) }

Java

// Creating two views to demonstrate the registration of the update listener.
final View view1 = findViewById(R.id.view1);
final View view2 = findViewById(R.id.view2);

// Setting up a spring animation to animate the view1 and view2 translationX and translationY properties
final SpringAnimation anim1X = new SpringAnimation(view1,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim1Y = new SpringAnimation(view1,
    DynamicAnimation.TRANSLATION_Y);
final SpringAnimation anim2X = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_X);
final SpringAnimation anim2Y = new SpringAnimation(view2,
        DynamicAnimation.TRANSLATION_Y);

// Registering the update listener
anim1X.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

// Overriding the method to notify view2 about the change in the view1’s property.
    @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2X.animateToFinalPosition(value);
    }
});

anim1Y.addUpdateListener(new DynamicAnimation.OnAnimationUpdateListener() {

  @Override
    public void onAnimationUpdate(DynamicAnimation dynamicAnimation, float value,
                                  float velocity) {
        anim2Y.animateToFinalPosition(value);
    }
});

OnAnimationEndListener

OnAnimationEndListener notifica el final de una animación. Puedes configurar el objeto de escucha para que reciba una devolución de llamada cada vez que la animación alcance el equilibrio o se cancele. Para registrar el objeto de escucha, sigue estos pasos:

  1. Llama al método addEndListener() y adjunta el objeto de escucha a la animación.
  2. Anula el método onAnimationEnd() para recibir una notificación cada vez que una animación alcance el equilibrio o se cancele.

Cómo quitar objetos de escucha

Para dejar de recibir devoluciones de llamada de actualización de animación y devoluciones de llamada de finalización de animación, llama a los métodos removeUpdateListener() y removeEndListener(), respectivamente.

Cómo establecer el valor de inicio de la animación

Para establecer el valor de inicio de la animación, llama al método setStartValue() y pasa el valor de inicio de la animación. Si no estableces el valor de inicio, la animación usa el valor actual de la propiedad del objeto como valor de inicio.

Cómo establecer un rango de valores de la animación

Puedes establecer los valores de animación mínimos y máximos cuando desees restringir el valor de la propiedad a un rango determinado. También ayuda a controlar el rango en caso de que animes propiedades que tienen un rango intrínseco, como alfa (de 0 a 1).

  • Para establecer el valor mínimo, llama al método setMinValue() y pasa el valor mínimo de la propiedad.
  • Para establecer el valor máximo, llama al método setMaxValue() y pasa el valor máximo de la propiedad.

Ambos métodos muestran la animación para la que se establece el valor.

Nota: Si estableciste el valor de inicio y definiste un rango de valores de animación, asegúrate de que el valor de inicio se encuentre dentro del rango de valores mínimo y máximo.

Cómo establecer la velocidad de inicio

La velocidad de inicio define la velocidad a la que cambia la propiedad de la animación al comienzo. La velocidad de inicio predeterminada se establece en cero píxeles por segundo. Puedes establecerla con la velocidad de los gestos táctiles o con un valor fijo como velocidad de inicio. Si eliges proporcionar un valor fijo, te recomendamos que lo definas en dp por segundo y luego lo conviertas a píxeles por segundo. Definir el valor en dp por segundo permite que la velocidad sea independiente de la densidad y los factores de forma. Si quieres obtener más información para convertir el valor a píxeles por segundo, consulta la sección Cómo convertir dp por segundo a píxeles por segundo.

Para establecer la velocidad, llama al método setStartVelocity() y pasa la velocidad en píxeles por segundo. El método muestra el objeto de fuerza de resorte en el que se establece la velocidad.

Nota: Usa los métodos de clase GestureDetector.OnGestureListener o VelocityTracker para recuperar y calcular la velocidad de los gestos táctiles.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Compute velocity in the unit pixel/second
        vt.computeCurrentVelocity(1000)
        val velocity = vt.yVelocity
        setStartVelocity(velocity)
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Compute velocity in the unit pixel/second
vt.computeCurrentVelocity(1000);
float velocity = vt.getYVelocity();
anim.setStartVelocity(velocity);

Cómo convertir dp por segundo a píxeles por segundo

La velocidad de un resorte debe expresarse en píxeles por segundo. Si eliges proporcionar un valor fijo como inicio de la velocidad, proporciona el valor en dp por segundo y, luego, conviértelo a píxeles por segundo. Para la conversión, usa el método applyDimension() de la clase TypedValue. Consulta el siguiente código de muestra:

Kotlin

val pixelPerSecond: Float =
    TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, resources.displayMetrics)

Java

float pixelPerSecond = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpPerSecond, getResources().getDisplayMetrics());

Cómo establecer las propiedades del resorte

La clase SpringForce define los métodos get y set para cada una de las propiedades del resorte, como la proporción de amortiguamiento y la rigidez. Para establecer las propiedades del resorte, es importante recuperar el objeto de fuerza de resorte o crear una fuerza de resorte personalizada en la que puedas establecer las propiedades. Para obtener más información sobre cómo crear una fuerza de resorte personalizada, consulta la sección Cómo crear una fuerza de resorte personalizada.

Sugerencia: Mientras usas los métodos set, puedes crear una cadena de métodos, ya que todos ellos devuelven el objeto de fuerza de resorte.

Proporción de amortiguamiento

La proporción de amortiguamiento describe una reducción gradual en la oscilación de un resorte. Si usas la proporción de amortiguamiento, puedes definir la rapidez con la que decaen las oscilaciones de un rebote al siguiente. Hay cuatro formas diferentes de amortiguar un resorte:

  • El sobreamortiguamiento se produce cuando la proporción de amortiguamiento es mayor que uno. Permite que el objeto vuelva suavemente a la posición de reposo.
  • El amortiguamiento crítico se produce cuando la proporción de amortiguamiento es igual a uno. Permite que el objeto regrese a la posición de reposo en el menor tiempo posible.
  • El subamortiguamiento se produce cuando la proporción de amortiguamiento es menor que uno. Permite que el objeto se sobrepase varias veces cuando pasa la posición de reposo y, luego, alcanza gradualmente la posición de reposo.
  • El no amortiguamiento se produce cuando la proporción de amortiguamiento es igual a cero. Permite que el objeto oscile de forma permanente.

Para agregar la proporción de amortiguamiento al resorte, sigue estos pasos:

  1. Llama al método getSpring() para recuperar el resorte para agregar la proporción de amortiguamiento.
  2. Llama al método setDampingRatio() y pasa la proporción de amortiguamiento que quieres agregar al resorte. El método muestra el objeto de fuerza de resorte en el que se establece la proporción de amortiguamiento.

    Nota: La proporción de amortiguamiento debe ser un número no negativo. Si estableces la proporción de amortiguamiento en cero, el resorte nunca alcanzará la posición de reposo. En otras palabras, oscilará de manera permanente.

Las siguientes constantes de proporción de amortiguamiento están disponibles en el sistema:

Figura 2: Rebote alto

Figura 3: Rebote medio

Figura 4: Rebote bajo

Figura 5: Sin rebote

La proporción de amortiguamiento predeterminada se establece en DAMPING_RATIO_MEDIUM_BOUNCY.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the damping ratio to create a low bouncing effect.
        spring.dampingRatio = SpringForce.DAMPING_RATIO_LOW_BOUNCY
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the damping ratio to create a low bouncing effect.
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY);
…

Rigidez

La rigidez define la constante del resorte, que mide su resistencia. Un resorte rígido aplica más fuerza al objeto adjunto cuando el resorte no está en la posición de reposo. Para agregar la rigidez al resorte, sigue estos pasos:

  1. Llama al método getSpring() para recuperar el resorte al que le agregarás la rigidez.
  2. Llama al método setStiffness() y pasa el valor de rigidez que quieres agregar al resorte. El método muestra el objeto de fuerza de resorte en el que se estableció la rigidez.

    Nota: La rigidez debe ser un número positivo.

Las siguientes constantes de rigidez están disponibles en el sistema:

Figura 6: Rigidez alta

Figura 7: Rigidez media

Figura 8: Rigidez baja

Figura 9: Rigidez muy baja

La rigidez predeterminada se establece en STIFFNESS_MEDIUM.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Setting the spring with a low stiffness.
        spring.stiffness = SpringForce.STIFFNESS_LOW
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Setting the spring with a low stiffness.
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW);
…

Cómo crear una fuerza de resorte personalizada

Puedes crear una fuerza de resorte personalizada como alternativa a usar la fuerza de resorte predeterminada. La fuerza de resorte personalizada te permite compartir la misma instancia de fuerza de resorte en varias animaciones de resorte. Una vez que hayas creado la fuerza de resorte, puedes establecer propiedades como la proporción de amortiguamiento y la rigidez.

  1. Crearás un objeto SpringForce.

    SpringForce force = new SpringForce();

  2. Llama a los métodos correspondientes para asignar las propiedades. También puedes crear una cadena de métodos.

    force.setDampingRatio(DAMPING_RATIO_LOW_BOUNCY).setStiffness(STIFFNESS_LOW);

  3. Llama al método setSpring() para establecer el resorte en la animación.

    setSpring(force);

Cómo iniciar la animación

Existen dos maneras de iniciar una animación de resorte: llamando al start() o llamando al método animateToFinalPosition(). Ambos métodos deben ser llamados en el subproceso principal.

El método animateToFinalPosition() realiza dos tareas:

  • Establece la posición final del resorte.
  • Inicia la animación si no ha comenzado.

Dado que el método actualiza la posición final del resorte e inicia la animación si es necesario, puedes llamar a este método en cualquier momento para cambiar el curso de una animación. Por ejemplo, en una animación de resorte encadenada, la animación de una vista depende de otra. Para este tipo de animación, es más conveniente usar el método animateToFinalPosition(). Si usas este método en una animación de resorte encadenada, no necesitas preocuparte de si la animación que deseas actualizar a continuación está en ejecución.

En la Figura 10, se muestra una animación de resorte encadenada, en la que la animación de una vista depende de otra.

Demostración de resortes en cadena
Figura 10: Demostración de resortes en cadena

Para usar el método animateToFinalPosition(), llama al método animateToFinalPosition() y pasa la posición de reposo del resorte. También puedes establecer la posición de reposo del resorte si llamas al método setFinalPosition().

El método start() no establece el valor de la propiedad en el valor de inicio de inmediato. El valor de propiedad cambia con cada pulso de animación, que ocurre antes del pase de dibujo. Como resultado, los cambios se reflejan en el siguiente fotograma, como si los valores se configuraran de inmediato.

Kotlin

findViewById<View>(R.id.imageView).also { img ->
    SpringAnimation(img, DynamicAnimation.TRANSLATION_Y).apply {
        …
        // Starting the animation
        start()
        …
    }
}

Java

final View img = findViewById(R.id.imageView);
final SpringAnimation anim = new SpringAnimation(img, DynamicAnimation.TRANSLATION_Y);
…
// Starting the animation
anim.start();
…

Cómo cancelar la animación

Puedes cancelar la animación o puedes omitirla hasta el final. Una situación ideal en la que necesitas cancelar la animación o omitirla hasta el final es cuando una interacción del usuario exige que la animación finalice de inmediato. Esto ocurre principalmente cuando un usuario sale de una app de manera abrupta o la vista se vuelve invisible.

Existen dos métodos que puedes usar para finalizar la animación. El método cancel() finaliza la animación en el valor en el que se encuentra. El método skipToEnd() omite la animación hasta el valor final y, luego, la finaliza.

Antes de que puedas finalizar la animación, es importante verificar primero el estado del resorte. Si el estado no está amortiguado, la animación nunca podrá alcanzar la posición de reposo. Para verificar el estado del resorte, llama al método canSkipToEnd(). Si el resorte está amortiguado, el método muestra true; de lo contrario, false.

Una vez que conozcas el estado del resorte, puedes finalizar una animación usando los métodos skipToEnd() o cancel(). Debes llamar al método cancel() solo en el subproceso principal.

Nota: En general, el método skipToEnd() provoca un salto visual.