Et kig på ripple effekten fra googles material design

Blandt mange af de ting som google præsenterede i forbindelse med deres material design, er den lille, men meget iøjenfaldende, ripple effekt – en dråbe lignende animation, der dukker frem på deres knapper og menuer når man klikker på dem.

Polymer projektet har implementeret den, men du behøver ikke et helt framework for at få den på din hjemmeside. Her er bud på hvordan den kan laves med plain ol’ vanilla Javascript og CSS.

Vi sætter effekten på nogle knapper i en vertikal menu. Her er markup’en til menuen, og på knappen er der tilføjet et span tag til vores ripple effekt.

<ul class="vertical-menu">
  <li class="vertical-menu-item">
    <a href="#" class="vertical-menu-btn">
      Menu 1
      <span class="ripple"></span>
    </a>
  </li>
  ....
</ul>

CSS’en til menuen er der intet specielt ved, udover den transition effekt der er tilføjet til knappen. Den benytter sig af en cubic-bezier timing function på background for at få baggrundsfarven frem hurtigt og fadeout langsomt:

.vertical-menu {
  list-style: none;
  line-height: 48px;
}
.vertical-menu-item {
  display: inline-block;
  position: relative;

}
.vertical-menu-btn {
  display: block;
  padding: 0 1rem;
  text-decoration: none;
  position: relative;
  overflow: hidden;
  background-color: #f2f2f2;
  &:active { /* Sass snip */
    background-color: #9c27b0;
  }
  color: #333;
  transition: background 0.6s cubic-bezier(.0,.0,.22,.88);
}

Og så har der sneget sig en lille Sass ting ind i eksemplet, &:active, som er en lille genvej til at skrive .vertical-menu-btn:active.

Selve ripplen laves med:

.ripple {
  width: 48px;
  height: 48px;
  position: absolute;
  top: 0;
  left: 0;
  /* Ripplen skal ikke vises før der klikkes, så den skaleres til 0 */
  transform: translate(-50%, -50%) scale(0, 0); 
  border-radius: 50%; /* Laver cirklen */
  background-color: #9c27b0;
  opacity: 1;
}

Vores ripple bliver her sat til samme højde som knappen og gjort rund med border-radius: 50%. Transform’en placerer center af ripplen i den position den har (0, 0) og gør ripplen usynlig ved at skalere den til 0.

Selve animationen er i en ripple-animated klasse:

.ripple-animate {
  transition: 0.6s transform ease-in-out, opacity 0.6s ease-in-out;
  opacity: 0;
  transform: translate(-50%, -50%) scale(2);
}

Klassen tilføjes så vha. lidt javascript der lytter på "click":

(function () {
  "use strict";

  var buttons = document.querySelectorAll(".vertical-menu-btn"),
      i, x, y, btn, btnBounds, ripple;

  function triggerRipple(evt) { 
    btn = evt.currentTarget;
    btnBounds = btn.getBoundingClientRect();
    x = evt.clientX - btnBounds.left;
    y = evt.clientY - btnBounds.top;
    
    ripple = btn.querySelector(".ripple");
    //Hvis knappen er klikket på før, 
    //så fjern ripple-animate for at genstarte den.
    ripple.classList.remove("ripple-animate");

    //Placer ripple der hvor musen ramte knappen.
    ripple.style.left = x + "px";
    ripple.style.top = y + "px";

    //Sætter startAnimation i kø for at browseren 
    //kan opdage ændringen.
    setTimeout(startAnimation, 0);
  }

  function startAnimation() {
    ripple.classList.add("ripple-animate");
  }

  for (i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener("click", triggerRipple);
  }
}());

Javascripten starter animationen ved at tilføje ".ripple-animate" klassen, men den flytter også ripplen ved at lytte på hvor man har klikket på knappen og sætter så position af ripplen udfra det.

Og det var faktisk det! Den kan gøres mere fleksibel ved at sætte størrelsen på ripple med Javascript udfra hvilket element den bliver placeret på, men for simpelthedens skyld er størrelsen "hardcoded" med CSS.

Men performer den nu også?

Det er altid værd lige at kigge i Chrome Inspector og køre lidt statistik på en så central komponenter på din hjemmeside - og faktisk så er der et potentielt issue med ovenstående Javascript kode.

Jeg optog i et klik i inspectorens timeline og her er resultatet af de ting der sker når man nu klikker på knappen:

Timeline resultat af ripple

Kan du finde den potentielle lag trigger? Der er triggered en layout, også kendt som reflow. Chrome er så venlig at skrive hvilken linje der er synderen og i linje 18 finder vi: ripple.style.left = x + "px";.

Fordi vi sætter positionen af vores ripple med en absolut position så trigger vi browseren til at lave et reflow og dvs. browseren skal til at gentegne alle påvirkede DOM noder. I det her tilfælde er det ikke så mange fordi min markup er meget lille (Der er kun 2 knapper):

Et billede hvor mange nodes der bliver påvirket af en layout trigger.

Formodentlig kører det spitzenklasse på din I7 3.4 Ghz iMac, men hvad med din tablet? Eller telefonen? Uanset, så bør man være opmærksom på layout triggers og se om de udgør en potentiel falskehals.

Så hvordan fjerner vi så den layout trigger? Jo, i stedet for at bruge left og top position, så kunne vi jo bruge translate til at flytte ripplen med, for som vi kan se her csstriggers.com så trigger transform ikke layout.

Her er den modificerede Javascript:

function triggerRipple(evt) { 
  btn = evt.currentTarget;
  btnBounds = btn.getBoundingClientRect();
  x = evt.clientX - btnBounds.left;
  y = evt.clientY - btnBounds.top;
  
  ripple = btn.querySelector(".ripple");

  ripple.classList.remove("ripple-animate");
  ripple.style.opacity = "1";
  ripple.style.transform = "translate(-50%, -50%) translate(" + x + "px," + y + "px) scale(0, 0)";

  window.requestAnimationFrame(startAnimation);
}

function startAnimation() {
  ripple.classList.add("ripple-animate");
  ripple.style.transform = "translate(-50%, -50%) translate(" + x + "px," + y + "px,) scale(2, 2)";
  ripple.style.opacity = "0";
}

Så i stedet for at .ripple-animate satte vores translate og opacity, så sættes dette nu med Javascript og det eneste vores .ripple-animate nu gør er at sætte transition:

.ripple-animate {
  transition: 0.6s transform ease-in-out, opacity 0.6s ease-in-out;
}

Og et nyt kig på timelinen viser:

timeline efter optimering er uden layout

Ingen layout triggers mere!

Prisen er så mere Javascript. Så man bør vurderer det fra case til case. Personligt foretrækker jeg at holde alt stylingen i CSS og så tilføje/fjerne klasserne med Javascript, men nogle gange er det jo ikke muligt og så må man jo bare leve med det. I det mindste brugte jeg ikke jQuery til det :)

Held og lykke med din Ripple implementation!

Kommentarer

  1. Det er sgu godt nok cool! Takker – jeg glæder mig til at afprøve det.
    Demo-siden er til den ‘dårligt performende’ version, ikke?

Smid en kommentar

Felter markeret med * skal udfyldes og din e-mail-adresse vil ikke blive offentliggjort.