La classica interazione con una pagina HTML passa per mouse e tastiera. Con l'arrivo dei dispositivi touch abbiamo altri due strumenti lo stilo e le dita e la maggior parte dei browser mappa le interazioni touch su eventi del mouse.
Questo approccio però non consente di distinguere eventi generati effettivamente dal mouse da eventi artificialmente attribuiti ad un mouse. Tra l'altro in questo modo risulta impossibile rilevare caratteristiche specifiche dei nuovi dispositivi, come ad esempio la il grado di pressione sullo schermo o la presenza di più punti di contatto.
Ogni modalità di input ha le sue peculiarità e per gestirle opportunamente occorrerebbe prevedere tutti i diversi modelli di oggetti ed eventi, il che renderebbe più complessa la scrittura dell'interfaccia.
Pointer Events, un modello unificato
Per far fronte a questa problematica Microsoft ha definito un modello di gestione uniforme dell'interazione con le pagine Web con i diversi dispositivi di puntamento.
Invece di gestire singolarmente ciascun meccanismo di interazione, Microsoft ha pensato di unificare la gestione basandola su un modello astratto: Pointer Events. Questa tecnologia è stata sottoposta al W3C per la standardizzazione e si trova attualmente in stato di Working Draft. Sfruttando questo approccio lo sviluppatore non ha bisogno di preoccuparsi del tipo di dispositivo utilizzato per l'interazione, ma dovrà semplicemente concentrarsi sui Pointer, cioè sui punti di contatto tra un qualsiasi dispositivo di puntamento e lo schermo.
Il modello proposto consente di intercettare e gestire diversi eventi legati all'interazione tra il dispositivo di puntamento e lo schermo e di rilevare informazioni sul tipo di dispositivo e su alcune caratteristiche del punto di contatto. Il tutto con un approccio che si presenta come una sorta di evoluzione del modello di eventi del mouse, quindi un approccio già familiare agli sviluppatori.
In attesa dello standard
Come abbiamo detto, la tecnologia Pointer Events non è ancora uno standard e pertanto non è supportata da tutti i browser. In realtà allo stato attuale essa è supportata soltanto da Internet Explorer 10 e la stessa Microsoft ha sviluppato un prototipo Open Source per Webkit. Tuttavia se intendiamo utilizzare Pointer Events anche sui browser che non la supportano nativamente possiamo comunque ricorrere a hand.js, un polyfill sviluppato da David Catuhe che ci consente di avere la comodità di un approccio unico nella gestione dei diversi meccanismi di interazione con le pagine Web.
È sufficiente scaricare la libreria dal sito del progetto ed inserire un riferimento all'interno della pagina per avere un supporto uniforme a mouse, dita e penne nei nostri script:
<script src="hand.js"></script>
Hand.js implementa Pointer Events come se fosse supportato nativamente, pertanto prevede anche la possibilità di verificare l'abilitazione tramite la proprietà pointerEnabled
dell'oggetto navigator
:
if (navigator.pointerEnabled) {
...
} else {
alert("Supporto di Pointer Events non abilitato!");
}
Ovviamente se abbiamo inserito il riferimento alla libreria il controllo può apparire superfluo, ma è sempre opportuno adottare questo approccio in modo tale da abituarci a verificare sempre il supporto di una funzionalità da parte dei browser ed avere il codice pronto per l'eventuale futuro supporto nativo.
Vediamo un esempio pratico.
Un esempio pratico
Per avere un'idea delle potenzialità degli eventi Pointer e della libreria hand.js proviamo a realizzare una semplice applicazione che consenta di disegnare su una pagina Web con un dispositivo di puntamento, indipendentemente dal fatto che si tratti di un mouse, una penna o il tocco delle dita.
Definiamo innanzitutto l'area all'interno della quale vogliamo consentire il disegno:
<body>
<canvas id="writingArea"></canvas>
</body>
</html>
Quindi al caricamento della pagina abilitiamo il canvas
alla scrittura ad gestiamo gli eventi rilevanti per il nostro scopo:
writingArea = document.getElementById("writingArea");
writingArea.width = writingArea.clientWidth;
writingArea.height = writingArea.clientHeight;
context = writingArea.getContext("2d");
writingArea.addEventListener("pointerdown", onPointerDown, false);
writingArea.addEventListener("pointermove", onPointerMove, false);
writingArea.addEventListener("pointerup", onPointerUp, false);
writingArea.addEventListener("pointerout", onPointerUp, false);
Gli eventi che gestiremo riguardano il clic su un pulsante del mouse o il tocco sullo schermo con un dito o una penna (pointerdown
), il rilascio del punto di contatto dallo schermo (pointerup
), il movimento del dispositivo (pointermove
) e l'uscita dall'area di scrittura (pointerout
). A ciascuno di questi eventi è assegnato un gestore come mostrato di seguito:
var pointerDown = false;
var lastPosition = {};
var onPointerOut = function (evt) {
pointerDown = false;
};
var onPointerUp = function (evt) {
pointerDown = false;
};
var onPointerDown = function (evt) {
pointerDown = true;
lastPosition = { x: evt.clientX, y: evt.clientY };
};
Come possiamo vedere, questi gestori si limitano a tenere traccia dello stato del puntatore e della posizione sullo schermo sfruttando le proprietà clientX
e clientY
dell'evento.
Abbiamo previsto una variabile booleana pointerDown
che ci indica se il punto di contatto è attivo o meno, stato che si verifica in corrispondenza dell'evento pointerdown
. In pratica, questa variabile permette di sapere se stiamo disegnando all'interno dell'area dedicata.
L'altra variabile lastPosition
contiene un oggetto che rappresenta le coordinate dell'ultima posizione del puntatore.
Le informazioni contenute in queste variabili verranno sfruttati dal gestore dell'evento pointermove
:
var onPointerMove = function(evt) {
if (pointerDown) {
context.strokeStyle = "rgb(255, 0, 0)";
context.beginPath();
context.lineWidth = 2;
context.moveTo(lastPosition.x, lastPosition.y);
context.lineTo(evt.clientX, evt.clientY);
context.closePath();
context.stroke();
lastPosition = { x: evt.clientX, y: evt.clientY };
}
}
Nel caso in cui il puntatore coinvolto stia disegnando, cioè lo stato del puntatore risulti attivo, vengono assegnate le impostazioni del tratto del disegno e viene tracciata una linea rossa tra la posizione corrente e le ultime coordinate del puntatore rilevate. Dopo di che viene aggiorna la posizione del puntatore.
L'effetto finale è il tracciamento di una linea rossa sul canvas man mano che ci spostiamo col mouse, con un penna o un un dito.
Vediamo ora cosa succede con il multi-touch.
Interazione multi-touch
L'esempio che abbiamo visto mostra come utilizzare gli eventi Pointer per gestire con lo stesso codice l'interazione tramite il mouse, una penna o il tocco di un solo dito.
Diversi dispositivi prevedono il supporto multi-touch, cioè la possibilità di gestire più punti di contatto sullo schermo. Possiamo rivedere il codice del nostro esempio per gestire più punti di contatto sostituendo alle variabili pointerDown
e lastPosition
due array che conterranno lo stesso tipo di informazione ma per i diversi punti di contatto in azione. Avremo quindi:
var pointerDown = [];
var lastPosition = [];
var onPointerOut = function (evt) {
pointerDown[evt.pointerId] = false;
};
var onPointerUp = function (evt) {
pointerDown[evt.pointerId] = false;
};
var onPointerDown = function (evt) {
pointerDown[evt.pointerId] = true;
lastPosition[evt.pointerId] = { x: evt.clientX, y: evt.clientY };
};
Ciascun elemento degli array avrà come indice l'identificatore del puntatore, rilevato tramite la proprietà pointerId
e come valore il corrispondente stato booleano e le coordinate dell'ultima posizione.
Rivediamo anche la gestione dell'evento pointermove
alla luce di questo adeguamento:
var onPointerMove = function(evt) {
if (pointerDown[evt.pointerId]) {
context.strokeStyle = "rgb(255, 0, 0)";
context.beginPath();
context.lineWidth = 2;
context.moveTo(lastPosition[evt.pointerId].x, lastPositions[evt.pointerId].y);
context.lineTo(evt.clientX, evt.clientY);
context.closePath();
context.stroke();
lastPosition[evt.pointerId] = { x: evt.clientX, y: evt.clientY };
}
}
Un'ultima accortezza per rendere il tutto pienamente fruibile riguarda il comportamento predefinito di diversi dispositivi in presenza di eventi multi-touch. Infatti, molti dispositivi attivano in automatico funzioni come ad esempio lo zoom della pagina.
Per evitare che il tocco con più dita attivi funzioni predefinite invece di consentire il tracciamento di più linee contemporaneamente si può intervenire a livello di CSS con la seguente regola:
#writingArea {touch-action:none; -ms-touch-action:none;}
La proposta di standard introduce infatti la proprietà CSS touch-action attualmente supportata soltanto da Internet Explorer 10 con lo specifico vendor prefix. Dal momento che i browser rimuovono le regole CSS che non conoscono, questa regola rischierebbe di non avere effetto fino a quando la proprietà non verrà supportata nativamente. Per fortuna hand.js effettua il parsing dei fogli di stile associati alla pagina ed implementa il supporto della regola, seppur con qualche limitazione.
Rilevare informazioni sul dispositivo
Oltre a fornirci informazioni sulla posizione del punto di contatto con lo schermo, gli eventi Pointer danno informazioni su alcune caratteristiche del dispositivo di puntamento utilizzato dall'utente.
Ad esempio, la proprietà pointerType ci dice che tipo di dispositivo viene utilizzato: un mouse, una penna o il tocco delle dita.
La proprietà pressure ci fornisce informazioni sul grado di pressione esercitato sullo schermo dal dispositivo, rappresentandolo con un valore decimale compreso tra 0
(nessun contatto) e 1
(massima pressione). Naturalmente la pressione può essere rilevata soltanto per dispositivi che prevedono un contatto fisico con lo schermo. Per i dispositivi che non prevedono contatto fisico, come ad esempio il mouse, viene impostata una pressione costante fittizia di 0.5
.
Il seguente esempio di codice mostra come intercettare il tipo di dispositivo e la relativa pressione:
var onPointerMove = function(evt) {
if (pointerDown) {
...
var infoArea = document.getElementById("infoArea");
infoArea.innerHTML = "Dispositivo di puntamento: <b>" + evt.pointerType + "</b><br>Pressione: <b>" + evt.pressure + "</b>";
}
}
Possiamo rilevare le dimensioni del punto di contatto con lo schermo utilizzando le proprietà height e width, utili ad esempio se vogliamo creare effetti grafici dipendenti dalle dimensioni della superficie di contatto.
Gestire il multi-touch quando non serve
In un contesto multi-touch potremmo avere la necessità di prendere in considerazione un solo punto di contatto e di ignorare gli altri. Chiediamoci ad esempio cosa succede nel caso in cui nella prima versione del nostro esempio, quella che non prevedeva il multi-touch, interagiamo con la pagina su più punti contemporaneamente. Il risultato sarà un po' diverso da quello che ci aspettiamo, dal momento che tutti i punti di contatto genereranno un evento pointerdown
che non distingue lo specifico puntatore che l'ha generato.
Se non intendiamo prevedere un'interazione multi-touch ma vogliamo mantenere la logica della nostra applicazione anche per questa modalità, dobbiamo rilevare un solo punto di contatto ed ignorare gli altri.
Questo può essere fatto tramite la proprietà isPrimary che ci sonsente di rilevare quale punto di contatto è considerato primario. Per il mouse è considerato primario l'unico puntatore mentre per penne e touch è considerato primario il primo contatto con lo schermo.