Jugando con caminos

Recientemente ayudé con una animación de héroe en una aplicación, desafortunadamente aún no puedo compartir esta animación ... pero quería compartir lo que aprendí al hacerla. En esta publicación, explicaré cómo recrear esta fascinante animación de Dave "beesandbombs" Whyte, que demuestra muchas de las mismas técnicas:

Polygon Laps por beesandbombs

Mi primer pensamiento al mirar esto (lo que podría no ser una gran sorpresa para cualquiera que conozca mi trabajo) fue buscar un AnimatedVectorDrawable (AVD en adelante). Los AVD son geniales, pero no son adecuados para todas las situaciones, específicamente teníamos los siguientes requisitos:

  • Sabía que necesitaríamos dibujar un polígono, pero no nos habíamos decidido por la forma exacta. Los AVD son animaciones "precocidas", por lo que variar la forma requeriría volver a hacer la animación.
  • Como parte del aspecto de "seguimiento del progreso", solo queremos dibujar una parte del polígono. Los AVD son "dispara y olvida", es decir, no puedes desplazarte por ellos.
  • Queríamos mover otro objeto alrededor del polígono. Esto definitivamente se puede lograr con AVD ... pero de nuevo requeriría mucho trabajo por adelantado para calcular previamente la composición.
  • Queríamos controlar el progreso del objeto que se mueve alrededor del polígono por separado de la parte del polígono que se muestra.

En su lugar, opté por implementar esto como un Drawable personalizado, compuesto por objetos Path. Las rutas son una representación fundamental de una forma (¡que los AVD usan debajo del capó!) Y las API de Canvas de Android ofrecen un soporte bastante rico para crear efectos interesantes con ellas. Antes de pasar por algunos de estos, quiero dar un saludo a esta excelente publicación de Romain Guy que demuestra muchas de las técnicas que construyo en esta publicación:

Coordinación polar

Por lo general, al definir formas 2D, trabajamos en coordenadas (x, y) técnicamente conocidas como coordenadas cartesianas. Definen formas especificando puntos por su distancia desde el origen a lo largo de los ejes x e y. Una alternativa es el sistema de coordenadas polares que en su lugar define puntos por un ángulo (θ) y un radio (r) desde el origen.

Coordenadas cartesianas (izquierda) vs coordenadas polares (derecha)

Podemos convertir entre coords polares y cartesianos con esta fórmula:

val x = radio * Math.cos (ángulo);
val y = radio * Math.sin (ángulo);

Recomiendo esta publicación para obtener más información sobre las coordenadas polares:

Para generar polígonos regulares (es decir, donde cada ángulo interior es el mismo), las coordenadas polares son extremadamente útiles. Puede calcular el ángulo necesario para producir el número deseado de lados (ya que los ángulos interiores suman 360º) y luego usar múltiplos de este ángulo con el mismo radio para describir cada punto. Luego puede convertir estos puntos en coordenadas cartesianas en las que trabajan las API de gráficos. Aquí hay una función para crear una ruta que describe un polígono con un número dado de lados y radio:

fun createPath (lados: Int, radio: Float): Ruta {
  val path = Path ()
  ángulo val = 2.0 * Math.PI / lados
  path.moveTo (
      cx + (radio * Math.cos (0.0)). toFloat (),
      cy + (radio * Math.sin (0.0)). toFloat ())
  para (i en 1 hasta los lados) {
    path.lineTo (
        cx + (radio * Math.cos (ángulo * i)). toFloat (),
        cy + (radio * Math.sin (ángulo * i)). toFloat ())
    }
  path.close ()
  vía de retorno
}

Entonces, para recrear nuestra composición objetivo, podemos crear una lista de polígonos con diferentes números de lados, radio y colores. Polygon es una clase simple que contiene esta información y calcula la ruta:

polígonos val privados = listOf (
  Polígono (lados = 3, radio = 45f, color = 0xffe84c65.toInt ()),
  Polígono (lados = 4, radio = 53f, color = 0xffe79442.toInt ()),
  Polígono (lados = 5, radio = 64f, color = 0xffefefbb.toInt ()),
  ...
)

Pintura de ruta efectiva

Dibujar una ruta es simple usando Canvas.drawPath (ruta, pintura) pero el parámetro Paint admite un efecto de ruta que podemos usar para alterar cómo se dibujará la ruta. Por ejemplo, podemos usar un CornerPathEffect para redondear las esquinas de nuestro polígono o un DashPathEffect para dibujar solo una parte de la Ruta (consulte la sección "Trazado de ruta" de la publicación mencionada anteriormente para obtener más detalles sobre esta técnica):

Una técnica alternativa para dibujar una subsección de una ruta es usar PathMeasure # getSegment que copia una parte en un nuevo objeto Path. Utilicé la técnica de guión para animar los parámetros de intervalo y fase, lo que permitió posibilidades interesantes.

Al exponer los parámetros que controlan estos efectos como propiedades de nuestros dibujables, podemos animarlos fácilmente:

objeto PROGRESS: FloatProperty  ("progreso") {
  anular fun setValue (pld: PolygonLapsDrawable, progress: Float) {
    pld.progress = progreso
  }
  anular fun get (pld: PolygonLapsDrawable) = pld.progress
}
...
ObjectAnimator.ofFloat (polygonLaps, PROGRESS, 0f, 1f) .apply {
  duración = 4000L
  interpolador = LinearInterpolator ()
  repeatCount = INFINITE
  repeatMode = RESTART
}.comienzo()

Por ejemplo, aquí hay diferentes formas de animar el progreso de los trazados de polígonos concéntricos:

Apegarse al camino

Para dibujar objetos a lo largo del camino, podemos usar un PathDashPathEffect. Esto "marca" otra ruta a lo largo de una ruta, por lo que, por ejemplo, estampar círculos azules a lo largo de un polígono podría verse así:

PathDashPathEffect acepta parámetros de avance y fase, es decir, la brecha entre los sellos y qué tan lejos moverse a lo largo del camino antes del primer sello. Al establecer el avance a la longitud de la ruta completa (obtenida a través de PathMeasure # getLength), podemos dibujar un solo sello. Al animar la fase (aquí controlada por un parámetro dotProgress [0, 1]) podemos hacer que este sello único se mueva a lo largo del camino.

fase val = dotProgress * polygon.length
dotPaint.pathEffect = PathDashPathEffect (pathDot, polygon.length,
    fase, TRADUCIR)
canvas.drawPath (polygon.path, dotPaint)

Ahora tenemos todos los ingredientes para crear nuestra composición. Al agregar otro parámetro a cada polígono del número de "vueltas" que cada punto debe completar por ciclo de animación, producimos esto:

Una recreación del gif original como un Android dibujable

Puede encontrar la fuente de este dibujo aquí:

https://gist.github.com/nickbutcher/b41da75b8b1fc115171af86c63796c5b#file-polygonlapsdrawable-kt

Mostrar algo de estilo

El ojo de águila entre ustedes podría haber notado el parámetro final de PathDashPathEffect: Style. Esta enumeración controla cómo transformar el sello en cada posición en la que se dibuja. Para ilustrar cómo funciona este parámetro, el siguiente ejemplo utiliza un sello triangular en lugar de un círculo y muestra los estilos de traslación y rotación:

Comparación de estilo de traducción (izquierda) con rotación (derecha)

Tenga en cuenta que cuando se utiliza la traducción, el sello del triángulo siempre tiene la misma orientación (apuntando hacia la izquierda) mientras que con el estilo de rotación, los triángulos giran para permanecer tangenciales a la ruta.

Hay un estilo final llamado morph que en realidad transforma el sello. Para ilustrar este comportamiento, he cambiado el sello a una línea a continuación. Observe cómo las líneas se doblan al atravesar las esquinas:

Demostrando PathDashPathEffect.Style.MORPH

Este es un efecto interesante, pero parece tener dificultades en algunas circunstancias, como el comienzo del camino o las curvas cerradas.

Tenga en cuenta que puede combinar PathEffects usando ComposePathEffect, así es como el sello de ruta sigue las esquinas redondeadas aquí, componiendo un PathDashPathEffect con un CornerPathEffect.

Yendo en una tangente

Si bien lo anterior fue todo lo que necesitamos para recrear la composición de vueltas de polígono, mi desafío inicial en realidad requería un poco más de trabajo. Una desventaja de usar PathDashPathEffect es que los sellos solo pueden tener una forma y color únicos. La composición en la que estaba trabajando requería un marcador más sofisticado, así que tuve que ir más allá de la técnica de estampado del camino. En su lugar, utilizo un Drawable y calculo en qué parte del camino se debe dibujar para un progreso determinado.

Mover un VectorDrawable a lo largo de un camino

Para lograr esto, nuevamente utilicé la clase PathMeasure, que ofrece un método getPosTan para obtener coordenadas de posición y tangente a una distancia dada a lo largo de una ruta. Con esta información (y un poco de matemática), podemos traducir y rotar el lienzo para dibujar nuestro marcador dibujable en la posición y orientación correctas:

pathMeasure.setPath (polygon.path, false)
pathMeasure.getPosTan (markerProgress * polygon.length, pos, tan)
canvas.translate (pos [0], pos [1])
ángulo val = Math.atan2 (tan [1] .toDouble (), tan [0] .toDouble ())
canvas.rotate (Math.toDegrees (angle) .toFloat ())
marker.draw (lienzo)

Encuentra tu camino

Esperemos que esta publicación haya demostrado cómo un dibujo personalizado usando la creación y manipulación de rutas puede ser útil para crear efectos gráficos interesantes. La creación de un dibujo personalizado le brinda el control definitivo para alterar y animar diferentes partes de la composición de forma independiente. Este enfoque también le permite suministrar valores dinámicamente en lugar de tener que preparar animaciones preestablecidas. Me impresionó mucho lo que puede lograr con las API de ruta de Android y los efectos integrados, todos los cuales han estado disponibles desde la API 1.