Eventos de toque

Con el fin de proporcionar soporte de calidad para usuarios de interfaces táctiles, los eventos táctiles dan la posibilidad de interpretar la actividad de los dedos en pantallas táctiles o trackpads.

Definiciones

Superficie
La superficie sensible al tacto. Esta puede ser una pantalla o un trackpad.
Punto de toque
Un punto de contacto con la superficie. Esto podría ser un dedo (o un codo, oreja, nariz, o lo que sea, pero probablemente un dedo) o un stylus.

Interfaces

TouchEvent
Representa un evento que ocurre cuando el estado de los toques en la superficie cambian.
Touch
Represeta un único punto de contacto entre el usuario y la superficie táctil.
TouchList
Representa varios puntos de toque: esto se utiliza cuando el usuario tiene, por ejemplo, varios dedos en la superficie al mismo tiempo.
DocumentTouch
Contiene varios métodos para crear objetos de Touch y TouchList.

Ejemplo

Este ejemplo muestra múltiples puntos de toques al mismo tiempo, permitiendo al usuario dibujar en un <canvas> con más de un dedo a la vez. Esto funciona solamente en un navegador que soporte eventos táctiles.

Nota: El texto de abajo usa el término "dedo" cuando describe el contacto con la superficie, pero esto podría ser, por supuesto, también un stylus u otro método de contacto.

Configurando los eventos de manipulación

Cuando la página carga, la función startup() mostrada a continuación es llamada por nuestro atributo onload del elemento <body>.

function startup() {
  var el = document.getElementsByTagName("canvas")[0];
  el.addEventListener("touchstart", handleStart, false);
  el.addEventListener("touchend", handleEnd, false);
  el.addEventListener("touchcancel", handleCancel, false);
  el.addEventListener("touchleave", handleLeave, false);
  el.addEventListener("touchmove", handleMove, false);
}

Esto simplemente configura todos los detectores de eventos para nuestro elemento <canvas>, por lo que podremos manejar todos los eventos de toque cuando se produzcan.

Seguimiento de nuevos toques

Cuando un evento touchstart ocurre, indicando que un nuevo toque sobre la superficie se ha producido, la función handleStart() de a continuación es llamada.

function handleStart(evt) {
  evt.preventDefault();
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;
        
  for (var i=0; i<touches.length; i++) {
    ongoingTouches.push(touches[i]);
    var color = colorForTouch(touches[i]);
    ctx.fillStyle = color;
    ctx.fillRect(touches[i].pageX-2, touches[i].pageY-2, 4, 4);
  }
}

Esto llama a event.preventDefault() para mantener al navegador procesando el evento de toque (esto también previene que un evento del ratón o mouse sea entregado). Entonces obtenemos el contexto y lanzamos la lista de puntos de contacto cambiados de la propiedad TouchEvent.changedTouches del evento.

Después de ello, iteramos sobre todos los objetos Touch de la lista, insertándolo en una matriz de puntos de toque activos y dibujando el punto de inicio como un pequeño rectángulo; estamos usando una línea de 4 pixeles de ancho, por tanto estamos dibujando un cuadrado de 4 por 4 píxeles como punto de consistencia.

Dibujando mientras los eventos de toque se mueven

Cada vez que uno o más dedos se mueven, un evento touchmove es entregado, resultando en una llamada a nuestra función handleMove(). Su responsabilidad en este ejemplo es actualizar la información de toque cacheada y dibujar una línea desde la posición previa a la posición actual en cada toque.

function handleMove(evt) {
  evt.preventDefault();
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;
  
  ctx.lineWidth = 4;
        
  for (var i=0; i<touches.length; i++) {
    var color = colorForTouch(touches[i]);
    var idx = ongoingTouchIndexById(touches[i].identifier);

    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
    ctx.lineTo(touches[i].pageX, touches[i].pageY);
    ctx.closePath();
    ctx.stroke();
    ongoingTouches.splice(idx, 1, touches[i]);  // swap in the new touch record
  }
}

Esto se repite también en los toques cambiados, pero mira en nuestra matriz de información de toques cacheados la información previa de cada toque con el fin de determinar los puntos de inicio para cada nuevo segmento de línea de toques que será dibujada. Esto se hace mirando cada propiedad de los toques de Touch.identifier . Esta propiedad es un único entero para cada toque, y sigue siendo consistente para cada evento durante la duración del contacto de cada dedo con la superficie.

Esto nos permite conseguir las coordenadas de la posición previa de cada toque y usar el método apropiado de contexto para dibujar un segmento de línea uniendo dos posiciones a la vez.

Después de dibujar la línea, llamamos a  Array.splice() para reemplazar la información previa sobre el punto de toque con la información actual de la matriz ongoingTouches.

Manejando el final de un toque

Cuando el usuario levanta un dedo de la superficie, un evento touchend es enviado.  De igual manera, si el dedo se desliza fuera de nuestro lienzo, obtenemos un evento touchleave. Manejamos ambos casos de la misma manera: llamando a la función handleEnd() de abajo. Su trabajo es dibujar el último segmento de línea para cada toque que ha finalizado y remueve el punto de toque de la lista de toques en marcha.

function handleEnd(evt) {
  evt.preventDefault();
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;
  
  ctx.lineWidth = 4;
        
  for (var i=0; i<touches.length; i++) {
    var color = colorForTouch(touches[i]);
    var idx = ongoingTouchIndexById(touches[i].identifier);
    
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(ongoingTouches[i].pageX, ongoingTouches[i].pageY);
    ctx.lineTo(touches[i].pageX, touches[i].pageY);
    ongoingTouches.splice(i, 1);  // remove it; we're done
  }
}

Esto es muy similar a la función previa; la única diferencia real es que cuando llamamos a  Array.splice(), simplemente remueve la antigua entrada de la lista de toques en marcha, sin añadir la información actualizada. El resultado es que detenemos el seguimiento del punto de toque.

Manejando los toques cancelados

Si el dedo del usuario se equivoca en la Interfaz del navegador, o el toque necesita ser cancelado, el evento touchcancel es enviado, y llamamos a la función handleCancel() abajo.

function handleCancel(evt) {
  evt.preventDefault();
  var touches = evt.changedTouches;
  
  for (var i=0; i<touches.length; i++) {
    ongoingTouches.splice(i, 1);  // remove it; we're done
  }
}

Dado que la idea es cancelar el toque inmediatamente, simplemente lo removemos de la lista de toques en marcha sin dibujar un segmento de línea final.

Funciones de conveniencia

Este ejemplo usa dos funciones de convenience que deben mirarse brevemente para ayudar a que el resto del código sea más claro.

Seleccionando un color para cada toque

Con el fin de hacer que cada dibujo de toque se vea diferente, la función  colorForTouch() es usada para elegir un color basado en el identificador único de toque. Este identificador estará siempre entre 0 y un valor menos que el número de toques activos. Puesto que es muy improbable que alguna persona con más de 16 dedos use este demo, convertimos estos directamente en colores de escalas grises.

function colorForTouch(touch) {
  var id = touch.identifier;
  id = id.toString(16); // make it a hex digit
  return "#" + id + id + id;
}

El resultado de esta función es un string o cadena que puede ser usada cuando se llame a funciones <canvas> para configurar los colores de dibujos. Por ejemplo, para un valor Touch.identifier  de 10, el resultado string o cadena es "#aaa".

Encontrando un toque continuo

La función ongoingTouchIndexById() abajo explora mediante la matriz ongoingTouches para encontrar el toque que coincida con el identificador dado, luego devuelve los índices de toques a la matriz.

function ongoingTouchIndexById(idToFind) {
  for (var i=0; i<ongoingTouches.length; i++) {
    var id = ongoingTouches[i].identifier;
    
    if (id == idToFind) {
      return i;
    }
  }
  return -1;    // not found
}

Ver ejemplo en vivo

Consejos adicionales

Esta sección provee consejos adicionales sobre como manejar los eventos de toques en tu aplicación web.

Manejando los clics

Ya que la llamada preventDefault() en un touchstart o el primer evento touchmove de una serie impide que los eventos correspondientes eventos del mouse o ratón se disparen, es común llamar a preventDefault() en touchmove en lugar de touchstart. De esta manera, los eventos del ratón pueden todavía ser disparados y cosas como enlaces siguen funcionando. Alternativamente, algunos frameworks tienen que referirse a eventos de toque como eventos de ratón para este mismo propósito. (Este ejemplo es muy simplificado y podria resultar en un extraño comportamiento. Solo se diseñó como guía).

function onTouch(evt) {
  evt.preventDefault();
  if (evt.touches.length > 1 || (evt.type == "touchend" && evt.touches.length > 0))
    return;

  var newEvt = document.createEvent("MouseEvents");
  var type = null;
  var touch = null;
  switch (event.type) {
    case "touchstart": type = "mousedown"; touch = event.changedTouches[[0];
    case "touchmove":  type = "mousemove"; touch = event.changedTouches[[0];
    case "touchend":   type = "mouseup"; touch = event.changedTouches[0];
  }
  newEvt.initMouseEvent(type, true, true, event.originalTarget.ownerDocument.defaultView, 0,
    touch.screenX, touch.screenY, touch.clientX, touch.clientY,
    evt.ctrlKey, evt.altKey, evt.shirtKey, evt.metaKey, 0, null);
  event.originalTarget.dispatchEvent(newEvt);
}

Llamando a preventDefault() solo en un segundo toque

Una cosa para prevenir cosas como pinchZoom en una página es llamar a preventDefault() en el segundo toque de una serie. Este comportamiento no está bien definido en los eventos de toque, y resulta en diferentes comportamientos en diferentes navegadores (osea iOS evitará el zoom o acercamiento pero permitirá vista panorámica con ambos dedos. Android permitirá zoom o acercamiento pero no una panorámica. Opera and Firefox actualmente evita panorámica y zoom o acercamiento). Actualmente, no se recomienda depender de ningún comportamiento en particular en este caso, si no mas bien depender de una meta vista para evitar el zoom.

Compatibilidad de navegadores

Característica Chrome Firefox (Gecko) Internet Explorer Opera Safari
Soporte básico 22.0 18.0 (18.0) No support No support No support
Característica Android Chrome for Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile
Soporte básico (Yes) ? 6.0 (6.0) ? ? (Yes)

Notas de Gecko

La preferencia dom.w3c_touch_events.enabled puede ser utilizada para activar o desactivar el soporte de eventos de toque estándares; por defecto, están activados.

Gecko 12.0 note
(Firefox 12.0 / Thunderbird 12.0 / SeaMonkey 2.9)

Antes de Gecko 12.0 (Firefox 12.0 / Thunderbird 12.0 / SeaMonkey 2.9), Gecko no soportaba multi-toques; solo un toque cada vez era reportado.

Note: Antes de Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3), Gecko ofrecía una API de eventos de toque propietaria. Está API está obsoleta actualmente; deberías cambiar a esta."

Etiquetas y colaboradores del documento

 Colaboradores en esta página: wffranco, fscholz, teoli, Fjaguero, jvmjunior, maedca
 Última actualización por: wffranco,