Telas sensíveis a toque estão disponíveis em cada vez mais dispositivos, de celulares a telas de computadores. Seu aplicativo deve responder ao toque de forma intuitiva e atraente.
Telas sensíveis a toque estão disponíveis em cada vez mais dispositivos, de celulares a telas de computadores. Quando os usuários optam por interagir com sua interface, o app precisa responder ao toque de forma intuitiva.
Responder a estados de elementos
Você já tocou ou clicou em um elemento de uma página da Web e se perguntou se o site detectou a ação?
A simples alteração de cor de um elemento quando os usuários tocam ou interagem com partes da sua interface proporciona uma garantia básica de que seu site está funcionando. Isso não só alivia a frustração do usuário, mas também pode proporcionar uma sensação de que o site é ágil e responsivo.
Elementos do DOM podem herdar qualquer um dos seguintes estados: padrão, foco,
passar o cursor e ativo. Para mudar a interface para cada um desses estados, precisamos aplicar estilos
às seguintes pseudoclasses :hover
, :focus
e :active
, conforme mostrado abaixo:
.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;
}
Na maioria dos navegadores para dispositivos móveis, os estados hover e/ou focus serão aplicados a um elemento depois que ele for tocado.
Considere com cautela quais estilos definir e como eles ficarão após a finalização do toque do usuário.
Como suprimir estilos padrão de navegadores
Depois de adicionar estilos para os diferentes estados, você vai perceber que a maioria dos navegadores
implementa os próprios estilos em resposta ao toque de um usuário. Isso ocorre em grande parte porque, quando os dispositivos móveis foram lançados pela primeira vez, vários sites não tinham estilo para o estado :active
. Como resultado, muitos navegadores adicionaram
uma cor ou um estilo de destaque adicional para dar feedback ao usuário.
A maioria dos navegadores usa a propriedade CSS outline
para mostrar um círculo em volta de um
elemento quando ele é focado. Você pode suprimir essa propriedade da seguinte maneira:
.btn:focus {
outline: 0;
/* Add replacement focus styling here (i.e. border) */
}
O Safari e o Chrome adicionam uma cor de destaque de toque que pode ser impedida com a
propriedade CSS -webkit-tap-highlight-color
:
/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
-webkit-tap-highlight-color: transparent;
}
O Internet Explorer do Windows Phone tem um comportamento semelhante, mas ele pode ser suprimido com uma meta tag:
<meta name="msapplication-tap-highlight" content="no">
O Firefox tem dois efeitos colaterais a serem tratados.
A pseudoclasse -moz-focus-inner
, que adiciona um contorno a
elementos tocáveis, pode ser removida definindo border: 0
.
Se você estiver usando um elemento <button>
no Firefox, um gradiente será aplicado, que pode ser removido ao definir background-image: none
.
/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
background-image: none;
}
.btn::-moz-focus-inner {
border: 0;
}
Como desativar a seleção de usuários
Durante a criação da sua interface, pode haver cenários nos quais você quer que os usuários interajam com os elementos, mas suprimindo o comportamento padrão de seleção de texto com ações manter o botão pressionado ou arrastando o mouse pela interface.
É possível fazer isso com a propriedade CSS user-select
, mas saiba que fazer isso no conteúdo pode ser extremamente irritante para os usuários se eles quiserem selecionar o texto no elemento.
Portanto, use com cuidado e moderação.
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
Implementar gestos personalizados
Se você tiver uma ideia para implementar interações e gestos personalizados em seu site, tenha em mente dois tópicos:
- Como oferecer suporte a todos os navegadores.
- Como manter o frame rate alto.
Neste artigo, veremos exatamente esses tópicos abrangendo as APIs que precisamos oferecer suporte para atingir todos os navegadores e, em seguida, abordaremos como usamos esses eventos de forma eficiente.
Dependendo do que você quer fazer com o gesto, é provável que você queira que o usuário interaja com um elemento por vez ou que ele possa interagir com vários elementos ao mesmo tempo.
Vamos analisar dois exemplos neste artigo, ambos demonstrando suporte para todos os navegadores e como manter o frame rate alto.
O primeiro exemplo permitirá que o usuário interaja com um elemento. Nesse caso, pode ser pertinente que todos os eventos de toque sejam fornecidos ao elemento em questão, desde que o gesto seja iniciado nesse elemento. Por exemplo, mover o dedo para fora de um elemento que comporte o gesto deslizar ainda permite que o usuário controle esse elemento.
Isso é bastante útil, pois proporciona mais flexibilidade para o usuário, mas aplica uma restrição em como o usuário pode interagir com sua IU.
Se, no entanto, você espera que os usuários interajam com vários elementos ao mesmo tempo (usando multitoque), você deve restringir o toque ao elemento específico.
Isso é mais flexível para os usuários, mas complica a lógica de manipulação da interface e é menos resistente a erros do usuário.
Adicione listeners de eventos
No Chrome (versão 55 e posteriores), no Internet Explorer e no Edge,
PointerEvents
são a abordagem recomendada para implementar gestos personalizados.
Em outros navegadores, TouchEvents
e MouseEvents
são a abordagem correta.
Uma das melhores características de PointerEvents
é que esse recurso mescla diversos tipos de entrada,
incluindo eventos de mouse, toque e caneta, em um só conjunto de
callbacks. Os eventos a serem detectados são pointerdown
, pointermove
, pointerup
e pointercancel
.
Os equivalentes em outros navegadores são touchstart
, touchmove
,
touchend
e touchcancel
para eventos de toque. Se você quiser implementar
o mesmo gesto para entrada de mouse, será necessário implementar mousedown
,
mousemove
e mouseup
.
Em caso de dúvidas sobre quais eventos usar, confira esta tabela de eventos de toque, mouse e ponteiro.
O uso desses eventos requer a chamada do método addEventListener()
em um elemento
DOM, junto com o nome de um evento, uma função de callback e um booleano.
O booleano determina se você precisa capturar o evento antes ou depois
que outros elementos tenham tido a oportunidade de capturar e interpretar os
eventos. true
significa que você quer que o evento apareça antes de outros elementos.
Confira um exemplo de como detectar o início de uma interação.
// 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);
}
Processar interações com um único elemento
No pequeno snippet de código acima, adicionamos apenas o detector de evento inicial para eventos do mouse. O motivo disso é que a maioria dos eventos de mouse só é acionada quando o cursor está sendo passado sobre o elemento ao qual o detector foi adicionado.
TouchEvents
vai rastrear um gesto depois que ele for iniciado, independentemente de onde o
toque ocorre, e PointerEvents
vai rastrear eventos, independentemente de onde o toque
ocorre depois que chamarmos setPointerCapture
em um elemento DOM.
Para eventos move e end do mouse, adicionamos os detectores de evento no método de início do gesto e adicionamos os detectores ao documento, o que significa que eles podem rastrear o cursor até que o gesto seja concluído.
As etapas para implementar isso são as seguintes:
- Adicione todos os listeners de TouchEvent e PointerEvent. Para MouseEvents, adicione apenas o evento de início.
- Dentro do callback do gesto de início, vincule os eventos move e end do mouse ao
documento. Assim, todos os eventos do mouse são recebidos, não importando
se o evento ocorre no elemento original ou não. Para PointerEvents,
precisamos chamar
setPointerCapture()
no elemento original para receber todos os outros eventos. Em seguida, processe o início do gesto. - Processe os eventos de movimento.
- No evento de término, remova os detectores de eventos move e end do mouse do documento e encerre o gesto.
Confira abaixo um snippet do nosso método handleGestureStart()
, que adiciona os eventos move
e end ao 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);
O callback de end que adicionamos é handleGestureEnd()
, que remove os detectores de eventos move
e end do documento e libera a captura do ponteiro
quando o gesto é concluído, assim:
// 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);
Ao seguir esse padrão de adicionar o evento de movimento ao documento, se o usuário começar a interagir com um elemento e mover o gesto para fora do elemento, vamos continuar recebendo movimentos do mouse, independentemente de onde eles estão na página, porque os eventos estão sendo recebidos do documento.
Este diagrama mostra o que os eventos de toque estão fazendo conforme adicionamos os eventos move e end ao documento após o início de um gesto.
Resposta eficiente ao toque
Agora que já cuidamos dos eventos start e end, podemos responder aos eventos de toque.
Para qualquer um dos eventos "start" e "move", é possível extrair facilmente x
e y
de um evento.
O exemplo a seguir verifica se o evento é de um TouchEvent
, verificando se targetTouches
existe. Se existir, ele extrai o
clientX
e o clientY
do primeiro toque.
Se o evento for um PointerEvent
ou MouseEvent
, ele vai extrair clientX
e clientY
diretamente do próprio 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;
}
Um TouchEvent
tem três listas que contêm dados de toque:
touches
: lista de todos os toques atuais na tela, independentemente do elemento do DOM em que eles estão.targetTouches
: lista de toques atualmente no elemento DOM ao qual o evento está vinculado.changedTouches
: lista de toques com mudanças resultantes do evento ser acionado.
Na maioria dos casos, o targetTouches
oferece tudo o que você precisa e quer. (Para
mais informações sobre essas listas, consulte Listas de toques).
Usar requestAnimationFrame
Como os callbacks de eventos são acionados na linha de execução principal, queremos executar o menor código possível nos callbacks dos nossos eventos, mantendo a taxa de frames alta e evitando atrasos.
Ao usar requestAnimationFrame()
, temos uma oportunidade de atualizar a interface antes
que o navegador renderize um frame e isso nos ajudará a remover
parte do trabalho nos retornos de chamada de eventos.
Se você não conhece o requestAnimationFrame()
,
saiba mais aqui.
Uma implementação típica é salvar as coordenadas x
e y
dos
eventos de início e movimento e solicitar um frame de animação dentro do callback do
evento de movimento.
Na nossa demonstração, nós armazenamos a posição de toque inicial em handleGestureStart()
(procure 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);
O método handleGestureMove()
armazena a posição do evento
antes de solicitar um frame de animação, se necessário, transmitindo a função
onAnimFrame()
como callback:
this.handleGestureMove = function (evt) {
evt.preventDefault();
if (!initialTouchPos) {
return;
}
lastTouchPos = getGesturePointFromEvent(evt);
if (rafPending) {
return;
}
rafPending = true;
window.requestAnimFrame(onAnimFrame);
}.bind(this);
O valor onAnimFrame
é uma função que, quando chamada, altera a IU
para movê-la. Ao transmitir essa função para requestAnimationFrame()
,
informamos ao navegador para chamá-la pouco antes da atualização da página
(ou seja, pintar todas as alterações na página).
No callback handleGestureMove()
, verificamos inicialmente se rafPending
é falso,
o que indica se onAnimFrame()
foi chamado por requestAnimationFrame()
desde o último evento de movimento. Isso significa que temos apenas um requestAnimationFrame()
aguardando para ser executado em qualquer momento.
Quando nosso callback onAnimFrame()
é executado, definimos a transformação nos
elementos que queremos mover antes de atualizar rafPending
para false
, permitindo que
o próximo evento de toque solicite um novo frame de animação.
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;
}
Controlar gestos usando ações de toque
A propriedade CSS touch-action
permite controlar o comportamento de toque
padrão de um elemento. Nos nossos exemplos, usamos touch-action: none
para
impedir que o navegador faça qualquer coisa com o toque de um usuário, permitindo que
interceptemos todos os eventos de toque.
/* Pass all touches to javascript: */
button.custom-touch-logic {
touch-action: none;
}
Usar touch-action: none
é um pouco exagerado, pois ela impede todos
os comportamentos padrão do navegador. Em muitos casos, uma das opções
abaixo é uma solução melhor.
touch-action
permite desativar gestos implementados por um navegador.
Por exemplo, o IE 10 e versões mais recentes oferecem suporte a um gesto de dois toques para ativar o zoom. Ao definir um
touch-action
de manipulation
, você impede o comportamento padrão de
toque duplo.
Isso permite que você implemente um gesto de dois toques pessoalmente.
Confira abaixo uma lista de valores touch-action
usados com frequência:
Oferecer suporte a versões mais antigas do IE
Se quiser oferecer suporte ao IE10, você vai precisar processar versões do prefixo do fornecedor de
PointerEvents
.
Para verificar o suporte de PointerEvents
, você geralmente procuraria
window.PointerEvent
, mas no IE10, você procuraria
window.navigator.msPointerEnabled
.
Os nomes de eventos com prefixos de fornecedor são: 'MSPointerDown'
, 'MSPointerUp'
e 'MSPointerMove'
.
O exemplo abaixo mostra como verificar o suporte e mudar os nomes dos 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 mais informações, confira este artigo de atualizações da Microsoft.
Referência
Pseudoclasses para estados de toque
A referência definitiva para eventos de toque pode ser encontrada aqui: W3C Touch Events.
Eventos de toque, mouse e ponteiro
Esses eventos são os blocos de construção para adicionar novos gestos ao aplicativo:
Listas de toque
Cada evento de toque inclui três atributos de lista:
Como ativar o suporte ao estado active no iOS
Infelizmente, o Safari no iOS não aplica o estado active por padrão. Para
fazer com que ele funcione, adicione um listener de evento touchstart
ao corpo
do documento ou a cada elemento.
Isso deve ser feito em um teste de user-agent para que essa configuração só seja executada em dispositivos iOS.
Adicionar um touchstart ao corpo do documento tem a vantagem de ser aplicado a todos os elementos no DOM, mas isso pode causar problemas de desempenho ao rolar a página.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
document.body.addEventListener('touchstart', function() {}, false);
}
};
A alternativa é adicionar os detectores de touchstart a todos os elementos interativos da página, o que amenizará alguns dos problemas de desempenho.
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);
}
}
};