Interacciones con JavaScript y HammerJs

Implementa mejores interacciones de usuario con JavaScript y la librería HammerJs. Consigue mejorar la experiencia de usuario a tus aplicaciones web HTML5.

https://hammerjs.github.io/ Add touch gestures to your webapp.

See the Pen Crear swipe con JavaScript y HammerJs by Danivalldo (@Danivalldo) on CodePen.

¿Quieres aprender a hacer este ejercicio? Saltar directamente al tutorial

¿Para qué sirve HammerJs?

Una de las metodologías más habituales a la hora de comunicar APIs entre sí, es mediante la exposición y “escucha” de eventos.

Los eventos describen acciones que han ocurrido en una capa de software en un determinado momento. Estas, se propagan para que otras APIs las puedan detectar y ejecutar funciones como respuesta. 

Un ejemplo de este tipo de comunicación es la API web de los navegadores. Gracias a los eventos que expone, un desarrollador puede capturar acciones como el click que hace un usuario sobre un elemento del DOM. O el scroll del navegador. O bien un evento táctil, al tocar la pantalla de un smartphone.

Sin embargo, en ocasiones las APIs no cubren todo el espectro de eventos que un desarrollador pueda necesitar para su aplicación. Uno de estos casos es el de los eventos táctiles y de ratón que se dan en el navegador.

Los navegadores modernos permiten “escuchar” un significativo número de eventos de este tipo. A pesar de eso, no es posible detectar de forma nativa si un usuario ha hecho un gesto de “swipe”.

Para determinar si eventualmente esa acción ha ocurrido, sería necesario programar un complejo conjunto de instrucciones por encima de eventos más simples. Teniendo en cuenta cosas cómo el movimiento y tiempo de interacción del usuario.

Y precisamente ahí es donde HammerJs juega un papel clave. El principal objetivo de ésta librería es ofrecer al desarrollador una interfaz que simplifica la detección de eventos táctiles más complejos. Tales como “Pan”, “Pinch”, “Press”, “Rotate”, “Swipe” o “Tap”.

Dicho de otro modo, existe la forma de simplificar la creación de interacciones con JavaScript y HammerJs.

Una API clara para detectar nuevos eventos

HammerJs es un proyecto open source creado por Jorik Tangelder. Y a pesar de llevar años sin recibir actualizaciones, en su momento fué mantenido por un equipo de más de 80 personas. Hasta acumular más de 22.700 estrellas.

Esta librería de unos 21Kb, está disponible como “package” y se puede instalar a través del comando “npm install –save hammerjs”.

Por un lado, la API de HammerJs permite implementar cómodamente los eventos mencionados anteriormente. Todo mediante la creación de una instancia de la clase Hammer. A esta, se le pasa el elemento del DOM sobre el que se quieren escuchar dichos eventos.

Por otro, permite extender la detección de nuevos eventos personalizados, como por ejemplo un «tripletap», a través de la subclase “Manager”.

A continuación solo hace falta lanzar el método on, seguido del nombre del evento y de la función de callback que va a despertar.

La página oficial de HammerJs presenta unos ejemplos muy sencillos, pero no por eso menos claros. Ejemplos que se basan en mostrar en pantalla el nombre de los eventos que se van capturando consecutivamente. Aunque simple, con ello ya se puede intuir el potencial y las posibilidades que ofrece la librería.

Por poner un ejemplo, HammerJs sería un candidato perfecto para implementar el  característico «swipe» hacia la derecha o hacia la izquierda, popularizado por la conocida aplicación de contactos Tinder.

Crea tu propio Tinder

Y precisamente vamos a intentar crear ese tipo de comportamiento. Como veremos a continuación va ser posible crear esa y otras interacciones con JavaScript y HammerJs.

Vamos a empezar creando una estructura HTML con los elementos básicos.

Los contenedores anidados van a ser, un contenedor principal “.cards-container”. Y un contenedor que va a actuar de tarjeta con sus iconos de “like” y “descartar”.

  <body>
    <div class="cards-container">
      <div class="card active-card">
        <img src="https://icongr.am/material/react.svg?size=128&color=2e2e2e" alt="" class="logo">
        <div class="actions-container">
          <img src="https://icongr.am/material/close.svg?size=100&color=a63636" alt="" class="heart-icon">
          <img src="https://icongr.am/material/cards-heart.svg?size=80&color=a63636" alt="" class="cross-icon">
        </div>
      </div>
    </div>
  </body>

La parte de estilos CSS puede parecer un tanto compleja. Sin embargo, sencillamente se establecen colores y se posicionan los elementos dentro de la tarjeta.

html {
  height: 100%;
  body {
    height: 100%;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    background-color: rgb(237, 242, 247);
  }
}
.cards-container {
  width: 300px;
  margin: 0 auto;
  .card {
    width: 300px;
    height: 300px;
    padding: 20px 20px 20px 20px;
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: space-around;
    background-color: #fff;
    transition: transform 0.2s ease-in-out;
    box-shadow: 0px 1.5px 1.8px rgba(0, 0, 0, 0.017),
      0px 3.4px 4px rgba(0, 0, 0, 0.024), 0px 6px 7px rgba(0, 0, 0, 0.03),
      0px 9.5px 11.1px rgba(0, 0, 0, 0.035),
      0px 14.7px 17.2px rgba(0, 0, 0, 0.04),
      0px 22.9px 26.8px rgba(0, 0, 0, 0.046),
      0px 38px 44.5px rgba(0, 0, 0, 0.053), 0px 76px 89px rgba(0, 0, 0, 0.07);
    cursor: grab;
    border-radius: 5px;
    &.released-out {
      transition: all 0.3s ease-in-out;
      opacity: 0;
    }
    &.recovering {
      transition: none;
      transform: scale(0);
      opacity: 0;
    }
    &.accepted {
    }
    &.discarted {
    }
    &.dragging {
      transition: none;
      cursor: grabbing;
    }
    .actions-container {
      display: flex;
      padding: 0 20px 0 0;
      justify-content: space-around;
      align-items: center;
      background-color: #f1f1f1;
      margin-top: 20px;
      img {
        transform: scale(0.8);
      }
    }
    &:after {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      content: "";
    }
  }
}

El comportamiento, por supuesto, lo vamos a crear mediante JavaScript. Inicialmente importamos la librería HammerJs. Declaramos algunas variables y funciones de control. Y guardamos referencias a elementos del DOM.

import Hammer from "hammerjs";
let techIndex = 1;
const technologies = [
  "react.svg",
  "angularjs-plain.svg",
  "vuejs.svg",
  "css3-plain.svg",
  "webpack.svg",
  "nodejs.svg",
  "sass.svg",
  "jquery.svg",
  "npm.svg",
];
const lerp = (x, y, a) => x * (1 - a) + y * a;
const clamp = (a, min = 0, max = 1) => Math.min(max, Math.max(min, a));
const invlerp = (x, y, a) => clamp((a - x) / (y - x));
const range = (x1, y1, x2, y2, a) => lerp(x2, y2, invlerp(x1, y1, a));
const activeCard = document.querySelector(".active-card");
const heartIcon = activeCard.querySelector(".heart-icon");
const crossIcon = activeCard.querySelector(".cross-icon");

Seguidamente, instanciamos la clase principal de la librería, pasando el elemento principal del DOM.

const pannableCard = new Hammer(activeCard);

Para controlar qué sucede en cada momento de la interacción, capturamos tres eventos mediante el método “on()”.

El primer evento a capturar es “panstart”. Para detectar en qué momento empieza la interacción de “swipe”.

pannableCard.on("panstart", () => {
  console.log("panstart");
  activeCard.classList.add("dragging");
});

El segundo es “pan”. En este describiremos cómo afecta al elemento del DOM el movimiento de arrastrar del usuario.

pannableCard.on("pan", (ev) => {
  const lateralDistance = window.innerWidth / 2;
  const rotation = range(
    -1 * lateralDistance,
    lateralDistance,
    -45,
    45,
    ev.deltaX
  );
  const sizeIconHeart = range(
    -1 * lateralDistance,
    lateralDistance,
    1.6,
    0,
    ev.deltaX
  );
  const sizeIconCross = range(
    -1 * lateralDistance,
    lateralDistance,
    0,
    1.6,
    ev.deltaX
  );
  activeCard.style.transform = `translate3d(${ev.deltaX}px, 0px, 0px) rotate(${rotation}deg)`;
  heartIcon.style.transform = `scale(${sizeIconHeart})`;
  crossIcon.style.transform = `scale(${sizeIconCross})`;
});

Finalmente capturamos el evento “panend”. Aquí controlamos la respuesta a ejecutar al final de la interacción del usuario. Para ello, creamos una función de “callback” llamada “createNewCard()” para resetear la interactividad con otra tarjeta.

const createNewCard = () => {
  if (techIndex > technologies.length - 1) {
    techIndex = 0;
  }
  activeCard
    .querySelector(".logo")
    .setAttribute("src", `imgs/${technologies[techIndex]}`);
  techIndex++;
  activeCard.setAttribute("style", "");
  activeCard.classList.remove("released-out", "accepted", "discarted");
  activeCard.classList.add("recovering");
  window.setTimeout(() => {
    activeCard.classList.remove("recovering");
  }, 150);
};
pannableCard.on("panend", (ev) => {
  const lateralDistance = window.innerWidth / 2;
  activeCard.classList.remove("dragging");
  heartIcon.setAttribute("style", "");
  crossIcon.setAttribute("style", "");
  if (ev.deltaX > lateralDistance * 0.5 || ev.deltaX < lateralDistance * -0.5) {
    activeCard.classList.add("released-out");
    activeCard.classList.add(ev.deltaX > 0 ? "accepted" : "discarted");
    const rotation = range(
      -1 * lateralDistance,
      lateralDistance,
      -45,
      45,
      ev.deltaX
    );
    activeCard.style.transform = `translate3d(${
      ev.deltaX + 100 * (ev.deltaX > 0 ? 1 : -1)
    }px, 0px, 0px) rotate(${rotation + 10 * (ev.deltaX > 0 ? 1 : -1)}deg)`;
    window.setTimeout(() => {
      createNewCard();
    }, 500);
    return;
  }
  activeCard.setAttribute("style", "");
});

Lo que descubrí utilizando HammerJs

En una ocasión, hace bastantes años, tuve que programar una app donde en un determinado momento se mostraba una imagen en pantalla. El caso es que, el cliente pidió que se pudiera hacer zoom-in / zoom-out mediante el gesto de pinzado o «pinch«.

Recuerdo que tardé días para resolver esa funcionalidad, capturando la entrada y salida de los eventos táctiles, y estableciendo relaciones trigonométricas entre estos.

Meses después descubrí que con HammerJs lo habría tenido en unas pocas horas. Es más, incluso descubrí otras librerías que abordan funcionalidades similares, com por ejemplo DraggbleJs. Tal y como ya vimos en este otro artículo.

Este ha sido un pequeño vistazo a la librería HammerJs. A continuación os dejo enlaces a más recursos, así como el ejercicio subido al repositorio de Github de “Librerías Js”.

Nos vemos pronto, un abrazo desarrolladores!

Deja un comentario