Crea il carosello perfetto, parte 1

I caroselli sono un punto fermo di streaming e siti di e-commerce. Sia Amazon che Netflix li usano come strumenti di navigazione di primo piano. In questo tutorial, valuteremo la progettazione dell'interazione di entrambi e useremo i risultati per implementare la giostra perfetta.

In questa serie di tutorial, impareremo anche alcune funzioni di Popmotion, un motore di animazione JavaScript. Offre strumenti di animazione come le interpolazioni (utili per l'impaginazione), il puntatore (per lo scorrimento) e la fisica primaverile (per i nostri deliziosi tocchi finali.)

La prima parte valuterà come Amazon e Netflix hanno implementato lo scrolling. Implementeremo quindi un carosello che può essere scostato via touch.

Entro la fine di questa serie, avremo implementato scroll, touchpad, impaginazioni, barre di avanzamento, navigazione da tastiera e alcuni piccoli tocchi utilizzando la fisica delle molle. Saremo anche stati esposti ad alcune composizioni funzionali di base.

Perfezionare?

Cosa ci vuole per una giostra per essere "perfetta"? Deve essere accessibile da:

  • Topo: Dovrebbe offrire pulsanti precedenti e successivi facili da selezionare e non oscurare il contenuto.
  • Toccare: Dovrebbe tracciare il dito e quindi scorrere con lo stesso movimento di quando il dito si solleva dallo schermo.
  • Rotella di scorrimento: Spesso trascurato, l'Apple Magic Mouse e molti trackpad per laptop offrono uno scorrimento orizzontale uniforme. Dovremmo utilizzare quelle capacità!
  • Tastiera: Molti utenti preferiscono non utilizzare o non sono in grado di utilizzare un mouse per la navigazione. È importante rendere accessibile il nostro carosello in modo che anche gli utenti possano utilizzare il nostro prodotto.

Infine, faremo un ulteriore passo in più e rendere questo un pezzo di UX fiducioso e piacevole facendo sì che la giostra risponda in modo chiaro e viscerale alla fisica della molla quando il cursore ha raggiunto la fine.

Impostare

Per prima cosa, prendiamo l'HTML e il CSS necessari per costruire un carosello rudimentale mediante la forchetta di questo CodePen. 

La penna è configurata con Sass per la preelaborazione di CSS e Babel per la trascrizione di ES6 JavaScript. Ho anche incluso Popmotion, a cui si può accedere window.popmotion.

È possibile copiare il codice in un progetto locale se si preferisce, ma è necessario assicurarsi che il proprio ambiente supporti Sass ed ES6. Devi anche installare Popmotion con npm installa popmotion.

Creare un nuovo carosello

In qualsiasi pagina, potremmo avere molti caroselli. Quindi abbiamo bisogno di un metodo per incapsulare lo stato e la funzionalità di ciascuno.

Userò a funzione di fabbrica piuttosto che a classe. Le funzioni di fabbrica evitano la necessità di usare la confusione spesso Questo parola chiave e semplificherà il codice ai fini di questo tutorial.

Nel tuo editor JavaScript, aggiungi questa semplice funzione:

function carousel (container)  carousel (document.querySelector ('. container'));

Inseriremo il nostro codice specifico del carosello all'interno di questo giostra funzione.

Come e perché di scorrimento

Il nostro primo compito è quello di far scorrere la giostra. Ci sono due modi in cui possiamo fare questo:

Scorrimento nativo del browser

La soluzione ovvia sarebbe quella di impostare overflow-x: scroll sul cursore. Ciò consentirebbe lo scorrimento nativo su tutti i browser, inclusi i dispositivi touch e mouse wheel orizzontali.

Ci sono, tuttavia, degli svantaggi di questo approccio:

  • Il contenuto al di fuori del contenitore non sarebbe visibile, il che può essere restrittivo per il nostro design.
  • Limita anche i modi in cui possiamo usare le animazioni per indicare che siamo arrivati ​​alla fine.
  • I browser desktop avranno una brutta (ma accessibile!) Barra di scorrimento orizzontale.

In alternativa:

Animare translateX

Potremmo anche animare il carosello translateX proprietà. Questo sarebbe molto versatile in quanto saremmo in grado di implementare esattamente il design che ci piace. translateX è anche molto performante, a differenza del CSS sinistra proprietà che può essere gestita dalla GPU del dispositivo.

Sul lato negativo, dovremmo reimplementare la funzionalità di scorrimento utilizzando JavaScript. Questo è più lavoro, più codice.

In che modo Amazon e Netflix si avvicinano allo scorrimento?

Sia i caroselli Amazon che Netflix fanno diversi compromessi nell'affrontare questo problema.

Amazon anima il carosello sinistra proprietà in modalità "desktop". Animazione sinistra è una scelta incredibilmente scarsa, in quanto la modifica attiva un ricalcolo del layout. Si tratta di un utilizzo intensivo della CPU, e le macchine più vecchie faranno fatica a raggiungere 60 fps.

Chiunque abbia preso la decisione di animare sinistra invece di translateX deve essere un vero idiota (spoiler: ero io, nel 2012. Non eravamo così illuminati in quei giorni).

Quando rileva un dispositivo touch, il carousel utilizza lo scorrimento nativo del browser. Il problema con la sola abilitazione di questo in modalità "mobile" è l'assenza di utenti desktop con rotelle di scorrimento orizzontali. Significa anche che qualsiasi contenuto al di fuori del carosello dovrà essere visivamente troncato:

Netflix anima correttamente il carosello translateX proprietà, e lo fa su tutti i dispositivi. Ciò consente loro di avere un design che sanguina al di fuori del carosello:

Questo, a sua volta, consente loro di creare un disegno di fantasia in cui gli oggetti vengono ingranditi al di fuori dei bordi xey del carosello e gli oggetti circostanti si spostano dal loro modo:

Sfortunatamente, la reimplementazione di Netflix dello scrolling dei dispositivi touch non è soddisfacente: utilizza un sistema di impaginazione basato sui gesti che sembra lento e macchinoso. Non c'è nemmeno considerazione per le ruote di scorrimento orizzontali.

Possiamo fare di meglio Facciamo codice!

Scorrimento come un professionista

La nostra prima mossa è afferrare il .cursore nodo. Mentre ci siamo, prendiamo gli oggetti in esso contenuti in modo che possiamo capire la dimensione del cursore.

function carousel (container) const slider = container.querySelector ('. slider'); const items = slider.querySelectorAll ('. item'); 

Misurare il carosello

Possiamo calcolare l'area visibile del cursore misurandone la larghezza:

const sliderVisibleWidth = slider.offsetWidth;

Vogliamo anche la larghezza totale di tutti gli oggetti contenuti all'interno. Per mantenere il nostro giostra funzione relativamente pulita, poniamo questo calcolo in una funzione separata nella parte superiore del nostro file.

Usando getBoundingClientRect misurare il sinistra offset del nostro primo oggetto e il destra offset del nostro ultimo articolo, possiamo usare la differenza tra di loro per trovare la larghezza totale di tutti gli articoli.

function getTotalItemsWidth (items) const left = items [0] .getBoundingClientRect (); const right = items [items.length - 1] .getBoundingClientRect (); ritorno a destra - a sinistra; 

Dopo il nostro sliderVisibleWidth misurazione, scrivere:

const totalItemsWidth = getTotalItemsWidth (items);

Ora possiamo calcolare la distanza massima che consentirà al nostro carosello di scorrere. È la larghezza totale di tutti i nostri articoli, meno una piena larghezza del nostro cursore visibile. Questo fornisce un numero che consente di allineare l'elemento più a destra con il diritto del nostro cursore:

const maxXOffset = 0; const minXOffset = - (totalItemsWidth - sliderVisibleWidth);

Con queste misure sul posto, siamo pronti per iniziare a scorrere il nostro carosello.

Ambientazione translateX

Popmotion viene fornito con un renderer CSS per l'impostazione semplice e performante delle proprietà CSS. Inoltre ha una funzione di valore che può essere usata per tracciare numeri e, soprattutto (come vedremo presto), per interrogare la loro velocità.

Nella parte superiore del tuo file JavaScript, importali in questo modo:

const css, value = window.popmotion;

Quindi, sulla linea dopo aver impostato minXOffset, crea un renderer CSS per il nostro cursore:

const sliderRenderer = css (slider);

E creare un valore per monitorare l'offset x del nostro slider e aggiornare il cursore translateX proprietà quando cambia:

const sliderX = value (0, (x) => sliderRenderer.set ('x', x));

Ora, spostare il cursore orizzontalmente è semplice come scrivere:

sliderX.set (-100);

Provalo!

Tocca Scorri

Vogliamo che il nostro carosello inizi a scorrere quando a utentetrascina il cursore orizzontalmente e smette di scorrere quando un utente smette di toccare lo schermo. I nostri gestori di eventi avranno questo aspetto:

lascia agire; function stopTouchScroll () document.removeEventListener ('touchend', stopTouchScroll);  function startTouchScroll (e) document.addEventListener ('touchend', stopTouchScroll);  slider.addEventListener ('touchstart', startTouchScroll, passive: false);

Nel nostro startTouchScroll funzione, vogliamo:

  • Interrompe l'attivazione di qualsiasi altra azione sliderX.
  • Trova il punto di contatto dell'origine.
  • Ascolta il prossimo touchmove evento per vedere se l'utente sta trascinando verticalmente o orizzontalmente.

Dopo document.addEventListener, Inserisci:

if (action) action.stop ();

Questo fermerà qualsiasi altra azione (come il rotolo di slancio alimentato dalla fisica che implementeremo stopTouchScroll) dallo spostamento del cursore. Ciò consentirà all'utente di "catturare" immediatamente il dispositivo di scorrimento se scorre oltre un elemento o titolo su cui desidera fare clic.

Successivamente, dobbiamo memorizzare il punto di contatto dell'origine. Questo ci permetterà di vedere dove l'utente muove il dito dopo. Se si tratta di un movimento verticale, consentiremo lo scorrimento della pagina come al solito. Se si tratta di un movimento orizzontale, scorreremo invece il cursore.

Vogliamo condividere questo touchOrigin tra i gestori di eventi. Quindi dopo lascia agire; Inserisci:

let touchOrigin = ;

Di nuovo nel nostro startTouchScroll gestore, aggiungere:

const touch = e.touches [0]; touchOrigin = x: touch.pageX, y: touch.pageY;

Ora possiamo aggiungere un touchmove ascoltatore di eventi al documento per determinare la direzione di trascinamento in base a questo touchOrigin:

document.addEventListener ('touchmove', defineDragDirection);

Nostro determineDragDirection la funzione misurerà la posizione del tocco successivo, verificherà che sia effettivamente spostata e, in tal caso, misurerà l'angolo per determinare se è verticale o orizzontale:

function defineDragDirection (e) const touch = e.changedTouches [0]; const touchLocation = x: touch.pageX, y: touch.pageY; 

Popmotion include alcuni calcolatori utili per misurare cose come la distanza tra due coordinate x / y. Possiamo importare quelli come questo:

const calc, css, value = window.popmotion;

Quindi misurare la distanza tra i due punti è questione di usare il distanza calcolatrice:

const distance = calc.distance (touchOrigin, touchLocation);

Ora se il tocco è stato spostato, possiamo annullare questo listener di eventi.

se (! distanza) ritorna; document.removeEventListener ('touchmove', defineDragDirection);

Misurare l'angolo tra i due punti con il angolo calcolatrice:

const angle = calc.angle (touchOrigin, touchLocation);

Possiamo usarlo per determinare se questo angolo è un angolo orizzontale o verticale, passandolo alla funzione seguente. Aggiungi questa funzione all'inizio del nostro file:

function angleIsVertical (angle) const isUp = (angolo <= -90 + 45 && angle >= -90 - 45); const isDown = (angolo <= 90 + 45 && angle >= 90 - 45); return (isUp || isDown); 

Questa funzione ritorna vero se l'angolo fornito è compreso tra -90 +/- 45 gradi (verso l'alto) o 90 +/- 45 gradi (verso il basso). Quindi possiamo aggiungere un altro ritorno clausola se questa funzione restituisce vero.

se (angleIsVertical (angle)) restituisce;

Puntatore

Ora sappiamo che l'utente sta provando a scorrere il carosello, possiamo iniziare a tracciare il dito. Popmotion offre un'azione del puntatore che emetterà le coordinate x / y di un puntatore del mouse o del tocco.

Innanzitutto, importa pointer:

const calc, css, pointer, value = window.popmotion;

Per tenere traccia dell'input tattile, fornire l'evento originario a pointer:

action = pointer (e) .start ();

Vogliamo misurare l'iniziale X posizione del nostro puntatore e applicare qualsiasi movimento al cursore. Per questo, possiamo usare un trasformatore chiamato applyOffset.

I trasformatori sono funzioni pure che assumono un valore e lo restituiscono, sì trasformato. Per esempio: const double = (v) => v * 2.

const calc, css, pointer, transform, value = window.popmotion; const applyOffset = transform;

applyOffset è una funzione al curry. Ciò significa che quando lo chiamiamo, crea una nuova funzione che può quindi essere passata a un valore. Per prima cosa lo chiamiamo con un numero di cui vogliamo misurare l'offset, in questo caso il valore corrente di action.x, e un numero per applicare quell'offset a. In questo caso, questo è il nostro sliderX.

Quindi il nostro applyOffset la funzione sarà simile a questa:

const applyPointerMovement = applyOffset (action.x.get (), sliderX.get ());

Ora possiamo usare questa funzione nel puntatore produzione callback per applicare il movimento del puntatore al cursore.

action.output ((x) => slider.set (applyPointerMovement (x)));

Fermarsi, con stile

Il carosello ora è trascinabile al tatto! Puoi testarlo utilizzando l'emulazione del dispositivo negli Strumenti per sviluppatori di Chrome.

Sembra un po 'janky, giusto? Potrebbe essersi verificato uno scorrimento simile a prima: si solleva il dito e lo scorrimento si interrompe. O lo scorrimento si interrompe e quindi una piccola animazione prende il sopravvento per simulare una continuazione dello scorrimento.

Non lo faremo. Possiamo usare l'azione fisica in Popmotion per prendere la vera velocità di sliderX e applicare l'attrito ad esso per un periodo di tempo.

Innanzitutto, aggiungilo al nostro elenco sempre crescente di importazioni:

const calc, css, physics, pointer, value = window.popmotion;

Quindi, alla fine del nostro stopTouchScroll funzione, aggiungere:

if (action) action.stop (); action = physics (from: sliderX.get (), velocity: sliderX.getVelocity (), friction: 0.2) .output ((v) => sliderX.set (v)) .start ();

Qui, a partire dal e velocità vengono impostati con il valore corrente e la velocità di sliderX. Ciò garantisce che la nostra simulazione fisica abbia le stesse condizioni iniziali di partenza del movimento di trascinamento dell'utente.

attrito è impostato come 0.2. L'attrito è impostato come valore da 01, con 0 non avere alcun attrito e 1 essendo l'attrito assoluto. Prova a giocare con questo valore per vedere il cambiamento apportato al "feeling" del carosello quando un utente interrompe il trascinamento.

Numeri più piccoli lo faranno sentire più leggeri, e numeri più grandi renderanno il movimento più pesante. Per un movimento a scorrimento, mi sento 0.2 colpisce un buon equilibrio tra irregolare e pigro.

confini

Ma c'è un problema! Se hai giocato con il tuo nuovo carosello touch, è ovvio. Non abbiamo limitato il movimento, rendendo possibile letteralmente buttare via la tua giostra!

C'è un altro trasformatore per questo lavoro, morsetto. Questa è anche una funzione al curry, ovvero se la chiamiamo con un valore minimo e massimo, ad esempio 0 e 1, restituirà una nuova funzione. In questo esempio, la nuova funzione limiterà qualsiasi numero assegnato ad esso in mezzo 0 e 1:

morsetto (0, 1) (5); // restituisce 1

Innanzitutto, importa morsetto:

const applyOffset, clamp = transform;

Vogliamo utilizzare questa funzione di bloccaggio attraverso il nostro carosello, quindi aggiungi questa linea dopo averla definita minXOffset:

const clampXOffset = clamp (minXOffset, maxXOffset);

Stiamo andando a modificare i due produzione abbiamo impostato le nostre azioni utilizzando una composizione funzionale leggera con il tubo trasformatore.

Tubo

Quando chiamiamo una funzione, la scriviamo in questo modo:

foo (0);

Se vogliamo dare l'output di quella funzione a un'altra funzione, potremmo scrivere in questo modo:

bar (foo (0));

Questo diventa leggermente difficile da leggere e peggiora solo quando aggiungiamo sempre più funzioni.

Con tubo, possiamo comporre una nuova funzione al di fuori di foobar che possiamo riutilizzare:

const foobar = pipe (foo, bar); foobar (0);

È anche scritto in un inizio naturale -> ordine di finitura, che rende più facile da seguire. Possiamo usare questo per comporre applyOffsetmorsetto in una singola funzione. Importare tubo:

const applyOffset, clamp, pipe = transform;

Sostituisci il produzione richiamata del nostro pointer con:

pipe ((x) => x, applyOffset (action.x.get (), sliderX.get ()), clampXOffset, (v) => sliderX.set (v))

E sostituire il produzione callback di fisica con:

pipe (clampXOffset, (v) => sliderX.set (v))

Questo tipo di composizione funzionale può creare in modo abbastanza preciso processi descrittivi, passo-passo, con funzioni più piccole e riutilizzabili.

Ora, quando trascini e getti la giostra, non si sposterà oltre i suoi limiti.

L'arresto brusco non è molto soddisfacente. Ma questo è un problema per una parte successiva!

Conclusione

Questo è tutto per la parte 1. Finora, abbiamo dato un'occhiata ai caroselli esistenti per vedere i punti di forza e di debolezza dei diversi approcci allo scrolling. Abbiamo utilizzato il tracciamento di input e la fisica di Popmotion per animare in modo performante il nostro carosello translateX con scorrimento a sfioramento. Siamo stati anche introdotti alla composizione funzionale e alle funzioni curry.

Puoi prendere una versione commentata della "storia finora" su questo CodePen.

Nelle prossime puntate, vedremo:

  • scorrendo con la rotella del mouse
  • rimisurazione della giostra quando si ridimensiona la finestra
  • impaginazione, con accessibilità tastiera e mouse
  • tocchi deliziosi, con l'aiuto della fisica di primavera

Non vedo l'ora di vederti lì!