Leger med stier

Jeg hjalp for nylig med en heltanimation i en app - desværre kan jeg ikke dele denne animation endnu ... men jeg ville dele det, jeg lærte at gøre det. I dette indlæg går jeg igennem med at genskabe denne fascinerende animation af Dave 'bierbomber' Whyte, der demonstrerer mange af de samme teknikker:

Polygon Laps af bier og bomber

Min første tanke, når jeg kiggede på dette (hvilket måske ikke er meget overraskende for nogen, der kender mit arbejde) var at nå frem til en AnimatedVectorDrawable (AVD i det følgende). AVD'er er fantastiske, men de er ikke egnede til enhver situation - specifikt havde vi følgende krav:

  • Jeg vidste, at vi var nødt til at tegne en polygon, men vi havde ikke slået os til den nøjagtige form. AVD'er er "forbagt" -animationer, da en sådan ændring af formen kræver genfremstilling af animationen.
  • En del af aspekten 'progress tracking', vi ønsker kun at tegne en del af polygonen. AVD'er er 'brand-og-glem', dvs. du kan ikke skrubbe gennem dem.
  • Vi ville flytte et andet objekt rundt om polygonen. Dette er bestemt muligt med AVD'er ... men igen vil kræve en masse forhåndsarbejde for at beregne sammensætningen på forhånd.
  • Vi ønskede at kontrollere udviklingen af ​​objektet, der bevæger sig rundt om polygonen separat fra den del af polygonen, der vises.

I stedet valgte jeg at implementere dette som en brugerdefineret Drawable, der består af sti-objekter. Stier er en grundlæggende repræsentation af en form (som AVD'er bruger under hætten!), Og Android's Canvas API'er tilbyder temmelig rig støtte til at skabe interessante effekter med dem. Før jeg gennemgår nogle af disse, vil jeg gerne råbe dette fremragende indlæg fra Romain Guy, der demonstrerer mange teknikker, som jeg bygger på i dette indlæg:

Polær koordinering

Når vi definerer 2d-former, arbejder vi normalt i (x, y) koordinater teknisk kendt som kartesiske koordinater. De definerer figurer ved at specificere punkter efter deres afstand fra oprindelsen langs x- og y-akserne. Et alternativ er det polære koordinatsystem, som i stedet definerer punkter med en vinkel (θ) og en radius (r) fra oprindelsen.

Kartesiske koordinater (venstre) vs polære koordinater (højre)

Vi kan konvertere mellem polære og kartesiske snore med denne formel:

val x = radius * Math.cos (vinkel);
val y = radius * Math.sin (vinkel);

Jeg kan varmt anbefale dette indlæg for at lære mere om polære koordinater:

For at generere regelmæssige polygoner (dvs. hvor hver indre vinkel er den samme) er polære koordinater ekstremt nyttige. Du kan beregne den nødvendige vinkel for at fremstille det ønskede antal sider (da de indvendige vinkler er i alt 360º) og derefter bruge multipler af denne vinkel med den samme radius til at beskrive hvert punkt. Du kan derefter konvertere disse punkter til kartesiske koordinater, som grafiske API'er fungerer i. Her er en funktion til at oprette en sti, der beskriver en polygon med et givet antal sider og radius:

sjovt createPath (sider: Int, radius: Float): Sti {
  val sti = sti ()
  valvinkel = 2,0 * Math.PI / sider
  path.moveTo (
      cx + (radius * Math.cos (0,0)). toFloat (),
      cy + (radius * Math.sin (0,0)). toFloat ())
  for (i i 1 indtil sider) {
    path.lineTo (
        cx + (radius * Math.cos (vinkel * i)). toFloat (),
        cy + (radius * Math.sin (vinkel * i)). toFloat ())
    }
  path.close ()
  retursti
}

Så for at genskabe vores målsammensætning, kan vi oprette en liste over polygoner med forskellige antal sider, radius og farver. Polygon er en simpel klasse, der indeholder denne info og beregner stien:

private val polygoner = listOf (
  Polygon (sider = 3, radius = 45f, farve = 0xffe84c65.toInt ()),
  Polygon (sider = 4, radius = 53f, farve = 0xffe79442.toInt ()),
  Polygon (sider = 5, radius = 64f, farve = 0xffefefbb.toInt ()),
  ...
)

Effektiv sti maling

Tegning af en sti er enkel ved hjælp af Canvas.drawPath (sti, maling), men Paint-parameteren understøtter en PathEffect, som vi kan bruge til at ændre, hvordan stien vil blive tegnet. For eksempel kan vi bruge et hjørnePathEffect til at afrunde hjørnerne af vores polygon eller en DashPathEffect til kun at tegne en del af stien (se afsnittet 'Sti-sporing' i ovennævnte post for flere detaljer om denne teknik):

En alternativ teknik til tegning af en underafsnit af en sti er at bruge PathMeasure # getSegment, der kopierer en del til et nyt sti-objekt. Jeg brugte instrumentbrætteteknikken, da animering af intervallet og faseparametre aktiverede interessante muligheder.

Ved at udsætte parametrene, der kontrollerer disse effekter, som egenskaber ved vores tegnbare, kan vi let animere dem:

objekt PROGRESS: FloatProperty  ("fremgang") {
  tilsidesætte fun setValue (pld: PolygonLapsDrawable, fremgang: Float) {
    pld.progress = fremskridt
  }
  tilsidesætte fun get (pld: PolygonLapsDrawable) = pld.progress
}
...
ObjectAnimator.ofFloat (polygonLaps, PROGRESS, 0f, 1f) .applikér {
  varighed = 4000L
  interpolator = LinearInterpolator ()
  repeatCount = INFINITE
  repeatMode = RESTART
}.Start()

For eksempel er her forskellige måder at animere udviklingen af ​​de koncentriske polygonstier:

Hold dig til stien

For at tegne objekter langs stien kan vi bruge en PathDashPathEffect. Dette 'stempler' en anden sti langs en sti, så for eksempel at stempling af blå cirkler langs en polygon kan se sådan ud:

PathDashPathEffect accepterer forskuds- og faseparametre - det er afstanden mellem frimærker og hvor langt man skal bevæge sig langs stien før det første stempel. Ved at indstille forskuddet til længden af ​​hele stien (opnået via PathMeasure # getLength), kan vi tegne et enkelt stempel. Ved at animere fasen (her styret af en dotProgress-parameter [0, 1]) kan vi få dette enkelt stempel til at bevæge sig langs stien.

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

Vi har nu alle ingredienserne til at skabe vores komposition. Ved at tilføje en anden parameter til hver polygon med antallet af 'omgange', skal hvert punkt udfyldes pr. Animationssløjfe, producerer vi dette:

En genoprettelse af det originale gif som en Android-tegning

Du kan finde kilden til denne tegning her:

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

Vis noget stil

Ørnen, der kiggede blandt jer, har måske bemærket den endelige parameter til PathDashPathEffect: Style. Dette enum styrer, hvordan frimærket skal transformeres i hver position, det tegnes. For at illustrere, hvordan denne parameter fungerer, anvender eksemplet herunder et trekantet stempel i stedet for en cirkel og viser både oversæt og roter typografier:

Sammenligning af oversættestil (venstre) med rotation (højre)

Bemærk, at når du bruger oversætter trekantstemplet altid i samme retning (peger mod venstre), mens trekanterne roterer for at forblive tangentielle til stien med den roterende stil.

Der er en sidste stil kaldet morph, som faktisk transformerer stempelet. For at illustrere denne opførsel har jeg ændret frimærket til en linje nedenfor. Bemærk, hvordan linierne bøjes, når de krydser hjørnerne:

Demonstration af PathDashPathEffect.Style.MORPH

Dette er en interessant effekt, men ser ud til at kæmpe under nogle omstændigheder som starten af ​​stien eller trange hjørner.

Bemærk, at du kan kombinere PathEffects ved hjælp af en ComposePathEffect, det er sådan, at stempelstemplet følger de afrundede hjørner her ved at komponere en PathDashPathEffect med et CornerPathEffect.

At gå på en tangens

Mens ovennævnte var alt, hvad vi har brug for for at genskabe polygonkurssammensætningen, krævede min første udfordring faktisk lidt mere arbejde. En ulempe ved at bruge PathDashPathEffect er, at frimærkerne kun kan være en enkelt form og farve. Kompositionen, jeg arbejdede på, krævede en mere sofistikeret markør, så jeg måtte bevæge mig ud over stempelstempleteknikken. I stedet bruger jeg et tegnbart og beregner hvor langs stien det skal trækkes for en given fremgang.

Flytning af en VectorDrawable langs en sti

For at opnå dette brugte jeg igen PathMeasure-klassen, der tilbyder en getPosTan-metode til at få positionskoordinater og tangens i en given afstand langs en sti. Med denne information (og lidt matematik) kan vi oversætte og rotere lærredet for at tegne vores markør, der kan tegnes i den rigtige placering og retning:

pathMeasure.setPath (polygon.path, false)
pathMeasure.getPosTan (markerProgress * polygon.length, pos, tan)
canvas.translate (pos [0], pos [1])
val vinkel = Math.atan2 (tan [1] .toDouble (), tan [0] .toDouble ())
canvas.rotate (Math.toDegrees (vinkel) .toFloat ())
marker.draw (lærred)

Find din vej

Forhåbentlig har dette indlæg demonstreret, hvordan en brugerdefinerbar tegning ved hjælp af stedsoprettelse og -manipulation kan være nyttig til at opbygge interessante grafiske effekter. Oprettelse af en brugerdefineret tegning giver dig den ultimative kontrol til at ændre og animere forskellige dele af kompositionen uafhængigt. Denne fremgangsmåde giver dig også mulighed for dynamisk at levere værdier i stedet for at skulle forberede pre-canned animationer. Jeg var meget imponeret over, hvad du kan opnå med Android's Path APIs og de indbyggede effekter, som alle har været tilgængelige siden API 1.