Quallen in Compose bewegen: Animieren von Bildvektoren und Anwenden von AGSL RenderEffects

Nov 25 2022
Ich liebe es, inspirierenden Menschen im Internet zu folgen und zu sehen, was sie machen – eine dieser Personen ist Cassie Codes, sie macht unglaubliche Animationen für das Internet. Eines ihrer inspirierenden Beispiele ist diese niedliche animierte Qualle.

Ich liebe es, inspirierenden Menschen im Internet zu folgen und zu sehen, was sie machen – eine dieser Personen ist Cassie Codes , sie macht unglaubliche Animationen für das Internet. Eines ihrer inspirierenden Beispiele ist diese niedliche animierte Qualle .

Nachdem ich das gesehen und eine Weile davon besessen war, dachte ich immer wieder, dass dieses niedliche kleine Wesen auch in Compose zum Leben erweckt werden muss. Dieser Blogbeitrag beschreibt also, wie ich das in Jetpack Compose gemacht habe, den endgültigen Code finden Sie hier . Die Techniken hier sind natürlich nicht nur für Quallen relevant … jeder andere Fisch tut es auch! Nur ein Scherz – dieser Blogbeitrag behandelt:

  • Benutzerdefinierte Bildvektoren
  • Animieren von Bildvektorpfaden oder -gruppen
  • Anwenden eines Verzerrungsrauscheffekts auf ein Composable mit AGSL RenderEffect.

Analyse der SVG

Um diese Qualle zu implementieren, müssen wir zuerst sehen, woraus das SVG besteht – und versuchen, die verschiedenen Teile davon zu replizieren. Der beste Weg, um herauszufinden, was ein SVG zeichnet, besteht darin, verschiedene Teile davon zu kommentieren und das visuelle Ergebnis dessen zu sehen, was jeder Abschnitt des SVG darstellt. Dazu können Sie es entweder im oben verlinkten Codepen ändern oder ein SVG in einem Texteditor herunterladen und öffnen (es ist ein textlesbares Format).

Werfen wir also einen Überblick auf dieses SVG:

<!-- 
    Jellyfish SVG, path data removed for brevity 
--> 
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530.46 563.1">
  <defs>
  <filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
    <feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
    <feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
  </filter>    
  </defs>
  <g class="jellyfish" filter="url(#turbulence)">
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle" />
    <path class="tentacle" />
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="tentacle"/>
    <path class="face" />
    <path class="outerJelly"/>
    <path id="freckle" />
    <path id="freckle"/>
    <path id="freckle-4"/>
  </g>
  <g id="bubbles" fill="#fff">
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble" />
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble"/>
    <path class="bubble" />
  </g>
  <g class="jellyfish face">
    <path class="eye lefteye"  fill="#b4bebf" d=""/>
    <path class="eye righteye" fill="#b4bebf" d=""/>
    <path class="mouth" fill="#d3d3d3" opacity=".72"/>
  </g>
</svg>

  1. Pfade und Gruppen von Pfaden, aus denen das SVG besteht:
  2. Tentakel
  3. Gesicht – Klecks und äußeres Gelee
  4. Augen – das Belebte offen und geschlossen
  5. Blasen – zufällig um die Qualle herum animieren – die Größe und das Alpha animieren
  6. Nachdem wir nun verstanden haben, woraus dieses SVG besteht, machen wir uns daran, die statische Version in Compose zu rendern.

    Benutzerdefinierten ImageVector erstellen

    Compose hat ein Konzept eines ImageVector , in dem Sie einen Vektor programmgesteuert aufbauen können – ähnlich wie bei SVG. Für Vektoren/SVGs, die Sie nur ohne Änderung rendern möchten, können Sie auch ein VectorDrawable mithilfe von painterResource(R.drawable.vector_image) laden. Dadurch wird es in einen ImageVector konvertiert, den Compose rendert.

    Jetzt fragen Sie sich vielleicht – warum importieren Sie die Qualle nicht einfach als SVG in eine XML-Datei und laden sie mit hoch painterResource(R.drawable.jelly_fish)?

    Das ist eine großartige Frage – und es ist möglich, die Qualle auf diese Weise zu laden, indem der Turbulenzaspekt des SVG entfernt wird und das Bild mit einem geladenen XML gerendert wird (wie in der Dokumentation hier erklärt ). Aber wir wollen ein bisschen mehr mit den einzelnen Teilen des Pfads machen, wie Teile beim Klicken animieren und einen Rauscheffekt auf den Körper anwenden, also werden wir unseren ImageVectorprogrammatisch aufbauen.

    Um diese Qualle in Compose zu rendern, können wir die Pfaddaten (oder das „ d“-Tag auf dem Pfad) kopieren, aus denen der Fisch besteht, zum Beispiel hat das erste Tentakel die folgenden Pfaddaten:

    M226.31 258.64c.77 8.68 2.71 16.48 1.55 25.15-.78 8.24-5 15.18-7.37 23-3.1 10.84-4.65 22.55 1.17 32.52 4.65 7.37 7.75 11.71 5.81 21.25-2.33 8.67-7.37 16.91-2.71 26 4.26 8.68 7.75 4.34 8.14-3 .39-12.14 0-24.28.77-36 .78-16.91-12-27.75-2.71-44.23 7-12.15 11.24-33 7.76-46.83z
    

    • M, m : Bewegen zu
    • L, l, H, h, V, v : Linie zu
    • C, c, S, s : Kubische Bézierkurve to
    • Q, q, T, t: Quadratische Bézierkurve to
    • A, a: Ellipsenbogenkurve zu
    • Z, z — Schließt den Pfad

    Jetzt denkst du wahrscheinlich – muss ich im Kopf zeichnen und alle Positionen und Befehle von Hand kennen? Nein überhaupt nicht. Sie können in den meisten Designprogrammen – wie Figma oder Inkscape – einen Vektor erstellen und das Ergebnis Ihrer Zeichnung in eine SVG-Datei exportieren, um diese Informationen selbst zu erhalten. Wütend!

    Um den Vektor in Compose zu erstellen: rufen wir auf rememberVectorPainter, wodurch ein erstellt ImageVectorwird, und wir erstellen ein Groupaufgerufenes jellyfish, dann ein weiteres Groupaufgerufenes tentaclesund wir platzieren das erste Pathdarin für das erste Tentakel. Wir haben auch einen RadialGradientals Hintergrund für die ganze Qualle gesetzt.

    Und das Ergebnis des Folgenden ist ein kleines Tentakel, das auf dem Bildschirm mit einem radial verlaufenden Hintergrund gezeichnet wurde!

    Erstes Tentakel gerendert

    Wir wiederholen diesen Vorgang für alle Elemente des SVG – indem wir die Teile des Pfads aus der SVG-Datei nehmen und die Farbe und Alpha auf den zu zeichnenden Pfad anwenden, gruppieren wir die Pfade auch logisch in die Tentakel, das Gesicht, die Blasen usw.:

    Wir haben jetzt unser gesamtes Quallen-Rendering mit dem Obigen ImageVector:

    Ganzes statisches Quallen-Rendering in Compose

    ImageVector-Pfade und -Gruppen animieren

    Wir wollen Teile dieses Vektors animieren:

    • Die Qualle sollte sich langsam auf und ab bewegen
    • Die Augen sollten beim Klicken der Qualle blinzeln
    • Auf den Quallenkörper sollte ein Wackel-/Rauscheffekt angewendet werden.

    Sehen wir uns also an, wie wir einzelne Bits der ImageVector.

    Bewegen Sie die Qualle auf und ab

    Wenn wir uns den Codepen ansehen, können wir sehen, dass sich die Qualle mit einer Übersetzung nach oben und unten bewegt (y-Übersetzung). Um dies in Compose zu tun, erstellen wir einen unendlichen Übergang und ein translationY, das über 3000 Millisekunden animiert wird. Dann setzen wir die Gruppe mit den Quallen und das Gesicht so, dass es ein translationYhat. Dies erzeugt die Auf- und Ab-Animation.

    Übersetzung auf und ab

    Großartig – ein Teil der ImageVectorAnimation bewegt sich jetzt nach oben und unten, Sie werden feststellen, dass die Blasen in derselben Position bleiben.

    Augenzwinkern ️

    Wenn wir uns den Codepen ansehen, können wir sehen, dass auf jedem der Augen eine Animation scaleYund ist. opacityLassen Sie uns diese beiden Variablen erstellen und die Skala auf Groupund das Alpha auf anwenden Path. Wir werden diese auch nur beim Klicken auf die Qualle anwenden, um dies zu einer interaktiveren Animation zu machen.

    Wir erstellen zwei Animatables , die den Animationszustand halten, und eine Suspend-Funktion, die wir aufrufen, wenn Sie auf die Qualle klicken – wir animieren diese Eigenschaften, um die Augen zu skalieren und auszublenden.

    Wir haben jetzt eine niedliche blinkende Animation auf Klick – und unsere Qualle ist fast fertig!

    Blinkt beim Klicken auf ImageVector

    Anwenden eines Verzerrungs-/Rauscheffekts

    Wir haben also die meisten Dinge, die wir animieren wollen – die Bewegung nach oben und unten und das Blinken. Schauen wir uns an, wie der Körper der Qualle diesen wackeligen Effekt hat, der Körper und die Tentakel bewegen sich mit Geräuschen, die auf sie angewendet werden, um ihm ein Gefühl von Bewegung zu geben.

    Codepen: Jellyfish ohne Rauschen vs. mit angewendetem Rauschen

    Wenn wir uns das SVG und den Animationscode ansehen, können wir sehen, dass es verwendet wird feTurbulence, um Rauschen zu erzeugen, das dann als feDisplacementMap.

    <filter id="turbulence" filterUnits="objectBoundingBox" x="0" y="0" width="100%" height="100%">
        <feTurbulence data-filterId="3" baseFrequency="0.02 0.03" result="turbulence" id="feturbulence" type="fractalNoise" numOctaves="1" seed="1"></feTurbulence>
        <feDisplacementMap id="displacement" xChannelSelector="R" yChannelSelector="G" in="SourceGraphic" in2="turbulence" scale="13" />
      </filter>    
      </defs>
      <g class="jellyfish" filter="url(#turbulence)">
    

    Wir können AGSL- Shader verwenden, um dies zu erreichen, es ist erwähnenswert, dass dies nur von Tiramisu und höher (API 33+) unterstützt wird. Zuerst müssen wir einen Shader erstellen, der als Wobble fungiert, wir werden zunächst kein Rauschen verwenden – der Einfachheit halber stattdessen nur eine Mapping-Funktion.

    Die Shader arbeiten so, dass sie auf einzelne Pixel wirken – wir erhalten eine Koordinate ( fragCoord) und es wird erwartet, dass wir ein Farbergebnis erzeugen, das an dieser Koordinate gerendert wird. Unten ist der anfängliche Shader, den wir zum Transformieren des Composable verwenden werden:

    In unserem Fall sind die Eingaben, die wir verwenden, unsere aktuell gerenderten Pixel auf dem Bildschirm. Wir erhalten Zugriff darauf über die uniform shader contents;Variable, die wir als Eingabe senden. Wir nehmen die Eingabekoordinate ( fragCoord) und wenden einige Transformationen auf diese Koordinate an – verschieben sie mit der Zeit und führen im Allgemeinen einige Berechnungen an ihr durch, um sie zu verschieben.

    Dies erzeugt eine neue Koordinate. Anstatt also die genaue Farbe an der fragCoordPosition zurückzugeben, verschieben wir die Position, von der wir das Eingabepixel erhalten. Wenn wir zum Beispiel hätten return contents.eval(fragCoord), würde es keine Änderung hervorrufen – es wäre ein Pass-Through. Wir erhalten jetzt die Pixelfarbe von einem anderen Punkt des Composable – was einen wackeligen Verzerrungseffekt auf den Inhalt des Composable erzeugt.

    Um dies auf unserem Composable zu verwenden, können wir diesen Shader als RenderEffectauf den Inhalt des Composable anwenden:

    Wir verwenden createRuntimeShaderEffect, indem wir die WOBBLE_SHADERals Eingabe übergeben. Dies nimmt den aktuellen Inhalt des Composable und stellt ihn als Eingabe für den Shader mit dem Parameternamen „ contents“ bereit. Wir fragen dann den Inhalt innerhalb der WOBBLE_SHADER. Die timeVariable ändert das Wobbeln im Laufe der Zeit (Erstellen der Animation).

    Wenn wir dies ausführen, können wir sehen, dass das Ganze Imagejetzt verzerrt ist und etwas wackeliger aussieht – genau wie eine Qualle.

    Wackeln auf der ganzen Qualle

    Wenn wir möchten, dass der Effekt nicht auf das Gesicht und die Blasen angewendet wird, können wir diese in separate extrahieren ImageVectorsund die Anwendung des Rendereffekts auf diese Vektoren überspringen:

    Wackeln Angewandt, ohne das Gesicht zu beeinträchtigen

    Rauscheffekt anwenden

    Der oben angegebene Shader verwendet keine Rauschfunktion, um eine Verschiebung auf den Inhalt des Composable anzuwenden. Rauschen ist eine Möglichkeit, eine Verschiebung mit einer strukturierteren Zufallsfunktion anzuwenden. Eine solche Art von Rauschen ist Perlin-Rauschen (das feTurbulenceunter der Haube verwendet wird). So würde es aussehen, wenn wir das Ergebnis der Ausführung der Perlin-Rauschfunktion rendern:

    Perlin-Noise-Ausgang

    Wir verwenden den Rauschwert für jede Koordinate im Raum und verwenden ihn, um eine neue Koordinate im „ contents“-Shader abzufragen.

    Lassen Sie uns unseren Shader aktualisieren, um eine Perlin-Noise-Funktion zu verwenden (angepasst von diesem Github-Repo ). Wir werden es dann verwenden, um die Koordinatenabbildung von der Eingabekoordinate zur Ausgabekoordinate (dh eine Verschiebungsabbildung) zu bestimmen.

    Wenn wir diese Rauschfunktion anwenden, erhalten wir ein viel besseres Ergebnis! Die Qualle sieht aus, als würde sie sich im Wasser bewegen.

    Auf den Quallenkörper angewendetes Perlin-Rauschen

    Aber warum sollte ich das verwenden?

    An diesem Punkt fragen Sie sich vielleicht, das ist cool – aber sehr nischenhaft in seinem Anwendungsfall, Rebecca. Sicher – vielleicht machst du bei der Arbeit nicht jeden Tag eine animierte Qualle (wir können träumen, oder?). Kann jedoch RenderEffectsauf jeden zusammensetzbaren Baum angewendet werden, sodass Sie Effekte auf fast alles anwenden können, was Sie möchten.

    Warum möchten Sie zum Beispiel nicht, dass Ihr Verlaufstext oder der gesamte zusammensetzbare Bildschirm einen Rauscheffekt oder einen anderen AGSL-Effekt hat, den Ihr Herz begehrt?

    Perlin-Rauschen auf das gesamte Composable angewendet

    Schluß

    Daher haben wir in diesem Blogbeitrag viele interessante Konzepte behandelt – das Erstellen von benutzerdefinierten ImageVectorsaus SVGs, das Animieren von Teilen einer ImageVectorund das Anwenden von AGSL-Shadern RenderEffectsauf unsere Benutzeroberfläche in Compose.

    Den vollständigen Code der Qualle finden Sie hier . Weitere Informationen zu AGSL RenderEffects finden Sie in der Dokumentation oder im JetLagged-Beispiel für ein weiteres Verwendungsbeispiel.

    Wenn Sie Fragen haben, wenden Sie sich bitte an Mastodon androiddev.social/@riggaroo oder Twitter .

    Danke an Jolanda Verhoef , Nick Butcher , Florina Muntenescu , Romain Guy , Nader Jawad für das wertvolle Feedback zu diesem Beitrag.