Cada vez más dispositivos tienen pantallas táctiles, desde teléfonos hasta pantallas de escritorio. Tu app debe responder al tacto de forma intuitiva y atractiva.
Cada vez más dispositivos tienen pantallas táctiles, desde teléfonos hasta pantallas de computadoras de escritorio. Cuando los usuarios eligen interactuar con la IU, la app debe responder al tacto de forma intuitiva.
Cómo responder a estados de elementos
¿Alguna vez tocaste o hiciste clic en un elemento de una página web y te preguntaste si el sitio realmente lo detectó?
El simple hecho de alterar el color de un elemento cuando los usuarios tocan o interactúan con partes de la IU ofrece la seguridad básica de que el sitio funciona. Esto no solo disminuye la frustración, sino que también brinda un estilo ágil y responsivo.
Los elementos del DOM pueden heredar cualquiera de los siguientes estados: predeterminado, foco, desplazamiento y activo. Para cambiar nuestra IU en cada uno de estos estados, debemos aplicar estilos a las siguientes seudoclases :hover
, :focus
y :active
, como se muestra a continuación:
.btn {
background-color: #4285f4;
}
.btn:hover {
background-color: #296cdb;
}
.btn:focus {
background-color: #0f52c1;
/* The outline parameter suppresses the border
color / outline when focused */
outline: 0;
}
.btn:active {
background-color: #0039a8;
}
En la mayoría de los navegadores para dispositivos móviles, los estados de hover o hover se aplican a un elemento después de que se presiona.
Piensa detenidamente qué estilos establecerás y cómo se verán para los usuarios después de que terminen de tocar la pantalla.
Suprimir estilos predeterminados del navegador
Una vez que agregues estilos para los diferentes estados, notarás que la mayoría de los navegadores implementan sus propios estilos cuando el usuario lo toca. Esto se debe en gran medida a que, cuando se lanzaron los dispositivos móviles por primera vez, muchos sitios no tenían un diseño para el estado :active
. Como resultado, muchos navegadores agregaron color o estilo de resaltado adicional para brindar comentarios a los usuarios.
La mayoría de los navegadores usan la propiedad outline
de CSS para mostrar un anillo alrededor de un elemento cuando este está enfocado. Puedes suprimirlo de la siguiente manera:
.btn:focus {
outline: 0;
/* Add replacement focus styling here (i.e. border) */
}
Safari y Chrome agregan un color de resaltado cuando se presiona, que se puede evitar con la propiedad -webkit-tap-highlight-color
de CSS:
/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
-webkit-tap-highlight-color: transparent;
}
Internet Explorer en Windows Phone tiene un comportamiento similar, pero se suprime mediante una metaetiqueta:
<meta name="msapplication-tap-highlight" content="no">
Firefox tiene dos efectos secundarios que se deben controlar.
Configura border: 0
para quitar la seudoclase -moz-focus-inner
, que agrega un esquema de los elementos táctiles.
Si usas un elemento <button>
en Firefox, se aplica un gradiente que puedes quitar si configuras background-image: none
.
/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
background-image: none;
}
.btn::-moz-focus-inner {
border: 0;
}
Inhabilitando la selección de usuarios
Cuando creas tu IU, puede haber situaciones en las que quieras que los usuarios interactúen con tus elementos, pero quieras suprimir el comportamiento predeterminado de seleccionar texto cuando mantienes presionado o arrastras el mouse sobre la IU.
Puedes hacerlo con la propiedad user-select
de CSS, pero ten en cuenta que hacerlo en el contenido puede ser extremely exasperante para los usuarios si quieren seleccionar el texto en el elemento.
Por lo tanto, asegúrate de usarlo con precaución y moderación.
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
Cómo implementar gestos personalizados
Si tienes una idea para interacciones y gestos personalizados en tu sitio, hay dos temas que debes tener en cuenta:
- Cómo brindar compatibilidad con todos los navegadores
- Cómo mantener una velocidad de fotogramas alta
En este artículo, analizaremos exactamente estos temas, veremos las APIs que necesitamos admitir para todos los navegadores y, luego, veremos cómo usamos estos eventos de manera eficiente.
Según lo que quieras que haga tu gesto, es probable que quieras que el usuario interactúe con un elemento a la vez o que pueda interactuar con varios elementos al mismo tiempo.
Veremos dos ejemplos en este artículo, los cuales demuestran la compatibilidad con todos los navegadores y cómo mantener la velocidad de fotogramas alta.
El primer ejemplo permitirá al usuario interactuar con un elemento. En este caso, es posible que quieras que se otorguen todos los eventos táctiles a ese elemento, siempre que el gesto haya comenzado inicialmente en el elemento. Por ejemplo, mover un dedo fuera del elemento deslizable aún puede controlar el elemento.
Esto es útil, ya que le proporciona mucha flexibilidad al usuario, pero aplica una restricción sobre la forma en que el usuario puede interactuar con tu IU.
Sin embargo, si esperas que los usuarios interactúen con varios elementos al mismo tiempo (mediante la función multitáctil), debes restringir la función táctil al elemento específico.
Esto resulta más flexible para los usuarios, pero complica la lógica para manipular la IU y es menos resistente a los errores del usuario.
Cómo agregar objetos de escucha de eventos
En Chrome (versión 55 y posteriores), Internet Explorer y Edge,
PointerEvents
es el enfoque recomendado para implementar gestos personalizados.
En otros navegadores, TouchEvents
y MouseEvents
son el enfoque correcto.
La excelente función de PointerEvents
es que combina varios tipos de entrada (incluidos los eventos del mouse, los eventos táctiles y de lápiz) en un conjunto de devoluciones de llamada. Los eventos que se deben escuchar son pointerdown
, pointermove
, pointerup
y pointercancel
.
Los equivalentes en otros navegadores son touchstart
, touchmove
, touchend
y touchcancel
para los eventos táctiles. Si quisieras implementar el mismo gesto para la entrada del mouse, tendrías que implementar mousedown
, mousemove
y mouseup
.
Si tienes preguntas sobre qué eventos usar, consulta esta tabla de eventos táctiles, de mouse y puntero.
Para usar estos eventos, debes llamar al método addEventListener()
en un elemento del DOM, junto con el nombre de un evento, una función de devolución de llamada y un valor booleano.
El valor booleano determina si debes capturar el evento antes o después de que otros elementos hayan tenido la oportunidad de captar y de interpretar los eventos. (true
significa que quieres el evento antes que otros elementos).
Este es un ejemplo de escucha para el inicio de una interacción.
// Check if pointer events are supported.
if (window.PointerEvent) {
// Add Pointer Event Listener
swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
// Add Touch Listener
swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);
// Add Mouse Listener
swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}
Cómo controlar la interacción con un solo elemento
En el breve fragmento de código anterior, solo agregamos el objeto de escucha de eventos de inicio para eventos del mouse. El motivo es que los eventos del mouse solo se activarán cuando el cursor se desplace sobre el elemento al que se agrega el objeto de escucha de eventos.
TouchEvents
realizará un seguimiento de un gesto después de que se inicie, independientemente del lugar en el que se produzca el toque, y PointerEvents
hará un seguimiento de los eventos, independientemente de dónde se produzca el toque después de que llamemos a setPointerCapture
en un elemento del DOM.
Para los eventos de finalización y movimiento del mouse, agregamos los objetos de escucha de eventos en el método de inicio del gesto y los agregamos al documento, lo que significa que puede hacer un seguimiento del cursor hasta que se complete el gesto.
Los pasos para implementar esto son los siguientes:
- Agrega todos los objetos de escucha TouchEvent y PointerEvent. Para MouseEvents, agrega solo el evento de inicio.
- Dentro de la devolución de llamada de gesto de inicio, vincula los eventos de movimiento y finalización del mouse al documento. De esta manera, se recibirán todos los eventos del mouse, independientemente de si el evento ocurrió en el elemento original o no. Para PointerEvents, debemos llamar a
setPointerCapture()
en nuestro elemento original para recibir todos los demás eventos. Luego, controla el inicio del gesto. - Controla los eventos de movimiento.
- En el evento de finalización, quita del documento los objetos de escucha de movimiento y finalización del mouse, y finaliza el gesto.
A continuación, se muestra un fragmento de nuestro método handleGestureStart()
, que agrega los eventos de movimiento y finalización al documento:
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if(evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
La devolución de llamada de finalización que agregamos es handleGestureEnd()
, que quita los objetos de escucha de movimiento y de finalización del documento, y libera la captura del puntero cuando finaliza el gesto:
// Handle end gestures
this.handleGestureEnd = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 0) {
return;
}
rafPending = false;
// Remove Event Listeners
if (window.PointerEvent) {
evt.target.releasePointerCapture(evt.pointerId);
} else {
// Remove Mouse Listeners
document.removeEventListener('mousemove', this.handleGestureMove, true);
document.removeEventListener('mouseup', this.handleGestureEnd, true);
}
updateSwipeRestPosition();
initialTouchPos = null;
}.bind(this);
Si se sigue este patrón de agregar el evento de movimiento al documento, si el usuario comienza a interactuar con un elemento y mueve su gesto fuera de él, seguiremos recibiendo movimientos del mouse independientemente de dónde se encuentren en la página, ya que los eventos se reciben desde el documento.
En este diagrama, se muestra lo que hacen los eventos táctiles cuando agregamos los eventos de movimiento y finalización al documento una vez que comienza un gesto.
Responder al tacto con eficiencia
Ahora que ya resolvimos los eventos de inicio y finalización, podemos responder a los eventos táctiles.
Para cualquiera de los eventos de inicio y movimiento, puedes extraer fácilmente x
y y
de un evento.
En el siguiente ejemplo, se comprueba si el evento proviene de un TouchEvent
. Para ello, se verifica si targetTouches
existe. Si es así, extrae clientX
y clientY
del primer toque.
Si el evento es PointerEvent
o MouseEvent
, extrae clientX
y clientY
directamente del evento.
function getGesturePointFromEvent(evt) {
var point = {};
if (evt.targetTouches) {
// Prefer Touch Events
point.x = evt.targetTouches[0].clientX;
point.y = evt.targetTouches[0].clientY;
} else {
// Either Mouse event or Pointer Event
point.x = evt.clientX;
point.y = evt.clientY;
}
return point;
}
Un elemento TouchEvent
tiene tres listas que contienen datos táctiles:
touches
: Es la lista de todas las acciones táctiles actuales en la pantalla, independientemente del elemento DOM en el que se encuentren.targetTouches
: Es la lista de toques que se realizan actualmente en el elemento DOM al que está vinculado el evento.changedTouches
: Es la lista de los toques que se modificaron y que se activó el evento.
En la mayoría de los casos, targetTouches
te brinda todo lo que necesitas y quieres. (para obtener más información sobre estas listas, consulta Listas de contactos táctiles).
Usa requestAnimationFrame
Dado que las devoluciones de llamada de eventos se activan en el subproceso principal, debemos ejecutar la menor cantidad de código posible en las devoluciones de llamada para nuestros eventos a fin de mantener la velocidad de fotogramas alta y evitar los bloqueos.
Con requestAnimationFrame()
, tenemos la oportunidad de actualizar la IU justo antes de que el navegador intente dibujar un fotograma y nos ayudará a quitar parte del trabajo de nuestras devoluciones de llamada de eventos.
Si no conoces requestAnimationFrame()
, puedes obtener más información aquí.
Una implementación típica es guardar las coordenadas x
y y
de los eventos de inicio y movimiento, y solicitar un fotograma de animación dentro de la devolución de llamada del evento de movimiento.
En nuestra demostración, almacenamos la posición inicial del toque en handleGestureStart()
(busca initialTouchPos
):
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
El método handleGestureMove()
almacena la posición de su evento antes de solicitar un fotograma de animación si es necesario y pasa nuestra función onAnimFrame()
como la devolución de llamada:
this.handleGestureMove = function (evt) {
evt.preventDefault();
if (!initialTouchPos) {
return;
}
lastTouchPos = getGesturePointFromEvent(evt);
if (rafPending) {
return;
}
rafPending = true;
window.requestAnimFrame(onAnimFrame);
}.bind(this);
El valor onAnimFrame
es una función que, cuando se la llama, cambia nuestra IU para moverla. Cuando pasas esta función a requestAnimationFrame()
, le indicamos al navegador que la llame justo antes de que esté a punto de actualizar la página (es decir, que pinte cualquier cambio en la página).
En la devolución de llamada handleGestureMove()
, primero verificamos si rafPending
es falso, lo que indica si requestAnimationFrame()
llamó a onAnimFrame()
desde el último evento de movimiento. Esto significa que solo tenemos un requestAnimationFrame()
en espera de ejecución a la vez.
Cuando se ejecuta nuestra devolución de llamada a onAnimFrame()
, configuramos la transformación en cualquier elemento que queremos mover antes de actualizar rafPending
a false
, lo que permite que el siguiente evento táctil solicite un nuevo fotograma de animación.
function onAnimFrame() {
if (!rafPending) {
return;
}
var differenceInX = initialTouchPos.x - lastTouchPos.x;
var newXTransform = (currentXPosition - differenceInX)+'px';
var transformStyle = 'translateX('+newXTransform+')';
swipeFrontElement.style.webkitTransform = transformStyle;
swipeFrontElement.style.MozTransform = transformStyle;
swipeFrontElement.style.msTransform = transformStyle;
swipeFrontElement.style.transform = transformStyle;
rafPending = false;
}
Cómo controlar gestos con acciones táctiles
La propiedad touch-action
de CSS te permite controlar el comportamiento táctil predeterminado de un elemento. En nuestros ejemplos, usamos touch-action: none
para evitar que el navegador realice alguna acción con el toque del usuario, lo que nos permite interceptar todos los eventos táctiles.
/* Pass all touches to javascript: */
button.custom-touch-logic {
touch-action: none;
}
touch-action: none
es una opción esencial, ya que impide todos los comportamientos predeterminados del navegador. En muchos casos, es mejor usar alguna de las siguientes opciones.
touch-action
te permite inhabilitar los gestos que implementa un navegador.
Por ejemplo, IE10+ admite el gesto de presionar dos veces para hacer zoom. Si configuras un touch-action
de manipulation
, evitas el comportamiento predeterminado de presionar dos veces.
De esta manera, puedes implementar por tu cuenta un gesto de presionar dos veces.
A continuación, se muestra una lista de valores touch-action
de uso general:
Compatibilidad con versiones anteriores de IE
Si deseas admitir IE10, deberás controlar las versiones de PointerEvents
con prefijos del proveedor.
Para comprobar la compatibilidad de PointerEvents
, debes buscar window.PointerEvent
, pero en IE10, debes buscar window.navigator.msPointerEnabled
.
Los nombres de los eventos con prefijos del proveedor son 'MSPointerDown'
, 'MSPointerUp'
y 'MSPointerMove'
.
En el siguiente ejemplo, se muestra cómo verificar la compatibilidad y cambiar los nombres de los eventos.
var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';
if (window.navigator.msPointerEnabled) {
pointerDownName = 'MSPointerDown';
pointerUpName = 'MSPointerUp';
pointerMoveName = 'MSPointerMove';
}
// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if (window.PointerEvent || window.navigator.msPointerEnabled) {
window.PointerEventsSupport = true;
}
Para obtener más información, consulta este artículo de actualizaciones de Microsoft.
Reference
Seudoclases para estados táctiles
Puedes encontrar la referencia definitiva de los eventos táctiles aquí: Eventos táctiles de W3C.
Eventos táctiles, de mouse y del puntero
Estos eventos son los componentes básicos para agregar gestos nuevos a tu aplicación:
Listas de contactos
Cada evento táctil incluye tres atributos de lista:
Habilita la compatibilidad con el estado activo en iOS
Lamentablemente, Safari en iOS no aplica el estado activo de forma predeterminada. Para que funcione, debes agregar un objeto de escucha de eventos touchstart
al cuerpo del documento o a cada elemento.
Debes hacerlo mediante una prueba de usuario-agente, de modo que solo se ejecute en dispositivos iOS.
Agregar un inicio táctil al cuerpo tiene la ventaja de aplicarse a todos los elementos del DOM; sin embargo, esto puede presentar problemas de rendimiento cuando te desplazas por la página.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
document.body.addEventListener('touchstart', function() {}, false);
}
};
La alternativa es agregar los objetos de escucha de inicio táctil a todos los elementos de la página con los que se pueda interactuar. De esta forma, se aliviarán algunos de los problemas de rendimiento.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
var elements = document.querySelectorAll('button');
var emptyFunction = function() {};
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('touchstart', emptyFunction, false);
}
}
};