Creați Caruselul Perfect, Partea 1

Caruselile sunt o bază de site-uri de streaming și de e-commerce. Atât Amazon cât și Netflix le folosesc ca instrumente de navigare proeminente. În acest tutorial, vom evalua designul interacțiunii ambelor și vom folosi constatările noastre pentru implementarea caruselului perfect.

În această serie de tutori, vom învăța, de asemenea, câteva funcții ale Popmotion, un motor de mișcare JavaScript. Acesta oferă instrumente de animație cum ar fi tweens (utile pentru paginare), urmărirea pointerului (pentru derulare) și fizica de primăvară (pentru deliciile noastre de finisare.)

Partea 1 va evalua modul în care Amazon și Netflix au implementat defilare. Apoi vom implementa un carusel care poate fi derulat prin atingere.

Până la sfârșitul acestei serii, vom fi implementat parcurgerea roților și touchpad-ului, paginarea, barele de progres, navigarea de la tastatură și câteva mici atingeri utilizând fizica primăvară. De asemenea, vom fi expuși unor compoziții funcționale de bază.

Perfect?

Ce este necesar ca un carusel să fie "perfect"? Trebuie să fie accesibil prin:

  • Șoarece: Ar trebui să ofere butoanele anterioare și următoare care să poată fi ușor de făcut clic și să nu observe conținutul.
  • Atingere: Ar trebui să urmăriți degetul, apoi să derulați cu același impuls ca atunci când degetul se ridică de pe ecran.
  • Rotiță de derulare: Adesea trecute cu vederea, mouse-ul Apple Magic și multe trackpad-uri laptop oferă o derulare orizontală orizontală. Ar trebui să folosim aceste capacități!
  • Tastatură: Mulți utilizatori preferă să nu utilizeze sau nu pot utiliza un mouse pentru navigare. Este important să ne facem caruselul accesibil, astfel încât utilizatorii să poată folosi și produsul nostru.

În cele din urmă, vom face lucruri care să avanseze în continuare și să facă o bucată de UX încrezătoare și încântătoare, făcând caruselul să răspundă clar și visceral cu fizica primăvară atunci când cursorul a atins sfârșitul.

Înființat

Mai întâi, hai să obținem HTML și CSS necesare pentru a construi un carusel rudimentar prin forfecarea acestui CodePen. 

Penul este configurat cu Sass pentru preprocesarea CSS și Babel pentru transpunerea JavaScript ES6. Am inclus, de asemenea, Popmotion, cu care pot fi accesate window.popmotion.

Puteți copia codul într-un proiect local, dacă preferați, dar va trebui să vă asigurați că mediul dvs. suportă Sass și ES6. De asemenea, va trebui să instalați Popmotion cu npm install popmotion.

Crearea unui carusel nou

Pe o anumită pagină, s-ar putea să avem multe caruseluri. Așadar, avem nevoie de o metodă de încapsulare a stării și funcționalității fiecăruia.

O să folosesc a funcția din fabrică mai degrabă decât a clasă. Funcțiile din fabrică evită nevoia de a folosi confuzia frecventă acest și va simplifica codul pentru scopurile acestui tutorial.

În editorul JavaScript, adăugați această funcție simplă:

carusel funcțional (container)  carusel (document.querySelector ("container"));

Vom adăuga codul nostru carusel specific în interiorul lui carusel funcţie.

Hows și Whys de defilare

Prima noastră sarcină este să facem parcurgerea caruselului. Există două modalități prin care putem face acest lucru:

Descărcarea navigatorului nativ

Soluția evidentă ar fi stabilirea overflow-x: derulați pe cursor. Acest lucru ar permite derularea nativă pe toate browserele, inclusiv dispozitivele cu atingere și orizontale ale mouse-ului.

Există, totuși, dezavantaje ale acestei abordări:

  • Conținutul în afara containerului nu ar fi vizibil, ceea ce poate fi restrictiv pentru designul nostru.
  • De asemenea, limitează modurile în care putem folosi animațiile pentru a indica că am ajuns la final.
  • Browserele desktop vor avea o bară de derulare orizontală (deși accesibilă!).

Alternativ:

Anima translateX

Am putea anima și caruselul translateX proprietate. Acest lucru ar fi foarte versatil, deoarece vom putea să implementăm exact designul pe care îl dorim. translateX este, de asemenea, foarte performant, spre deosebire de CSS stânga proprietatea poate fi manipulată de GPU-ul dispozitivului.

În partea inferioară, va trebui să reimplementăm funcția defilare folosind JavaScript. E mai multă muncă, mai mult cod.

Cum defilează abordarea Amazon și Netflix?

Ambele caruseluri Amazon și Netflix fac diferite compromisuri în abordarea acestei probleme.

Amazon animă caruselul stânga proprietate în modul "desktop". însuflețitor stânga este o alegere incredibil de săracă, deoarece schimbarea acesteia declanșează o recalculare a aspectului. Acest lucru este CPU-intensiv, iar mașinile mai vechi se vor lupta pentru a atinge 60fps.

Oricine a luat decizia de a anima stânga in loc de translateX trebuie să fie un adevărat idiot (spoiler: am fost eu, înapoi în 2012. Nu am fost la fel de iluminat în acele zile.)

Când detectează un dispozitiv tactil, caruselul folosește derularea nativă a browserului. Problema care se activează numai în modul "mobil" este utilizarea utilizatorilor desktop cu roți orizontale de defilare. De asemenea, înseamnă că orice conținut în afara caruselului va trebui tăiat vizual:

Netflix animă în mod corect caruselul translateX de proprietate, și face acest lucru pe toate dispozitivele. Acest lucru le permite să aibă un design care sângerează în afara caruselului:

Aceasta, la rândul său, le permite să realizeze un design fantezist în care elementele sunt lărgite în afara marginilor x și y ale caruselului și obiectele din jur se deplasează din calea lor:

Din nefericire, reimplementarea de către Netflix a derulării pentru dispozitive touch este nesatisfăcătoare: folosește un sistem de paginare bazat pe gesturi, care se simte încet și greoi. De asemenea, nu există nici o considerație pentru roțile orizontale de defilare.

Putem face mai bine. Să codificăm!

Scrolling Like a Pro

Prima noastră mișcare este de a apuca .cursor nodul. În timp ce suntem la ea, să luăm elementele pe care le conține, astfel încât să putem înțelege dimensiunea glisorului.

carusel de funcții (container) const slider = container.querySelector ("slider"); const elemente = slider.querySelectorAll ('. item'); 

Măsurarea caruselului

Putem realiza zona vizibilă a cursorului prin măsurarea lățimii sale:

const sliderVisibleWidth = slider.offsetWidth;

Vom dori, de asemenea, lățimea totală a tuturor articolelor conținute în. Să ne păstrăm carusel funcția relativ curată, să punem acest calcul într-o funcție separată în partea de sus a fișierului nostru.

Prin utilizarea getBoundingClientRect pentru a măsura stânga offset de primul nostru element și dreapta offset de ultimul nostru element, putem folosi diferența dintre ele pentru a găsi lățimea totală a tuturor elementelor.

funcția getTotalItemsWidth (elemente) const left = elemente [0] .getBoundingClientRect (); const right = elemente [items.length - 1] .getBoundingClientRect (); întoarcere dreapta - stânga; 

După noi sliderVisibleWidth măsurare, scrieți:

const totalItemsWidth = getTotalItemsWidth (elemente);

Acum ne putem da seama de distanța maximă la care caruselul nostru ar trebui să aibă dreptul să parcurgă. Este lățimea totală a tuturor articolelor noastre, minus o lățime întreagă a glisorului nostru vizibil. Acesta oferă un număr care permite elementului din dreapta să se alinieze cu dreptul glisorului nostru:

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

Cu ajutorul acestor măsurători, suntem gata să începem să derulăm caruselul nostru.

reglaj translateX

Popmotion vine cu un renderer CSS pentru setarea simplă și performantă a proprietăților CSS. De asemenea, vine cu o funcție de valoare care poate fi utilizată pentru a urmări numerele și, important (așa cum vom vedea în curând), să interogăm viteza lor.

În partea de sus a fișierului JavaScript, importați-le astfel:

const css, value = fereastra.popmotion;

Apoi, pe linie după ce am stabilit minXOffset, creați un renderer CSS pentru glisorul nostru:

const sliderRenderer = css (cursor);

Și creați o valoare pentru a urmări offsetul culisorului nostru x și actualizați glisorul translateX proprietate când se schimbă:

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

Acum, deplasarea cursorului pe orizontală este la fel de simplă:

sliderX.set (-100);

Incearca-l!

Atingeți Derulați

Vrem ca caruselul nostru să înceapă derularea atunci când a utilizatortrage glisorul orizontal și pentru a opri derularea când un utilizator nu mai atinge ecranul. Operatorii noștri de evenimente vor arăta astfel:

permiteți acțiune; funcția stopTouchScroll () document.removeEventListener ("atingere", stopTouchScroll);  funcția startTouchScroll (e) document.addEventListener ('touchend', stopTouchScroll);  slider.addEventListener ("touchstart", startTouchScroll, pasiv: false);

În a noastră startTouchScroll , dorim sa:

  • Opriți orice alte acțiuni de alimentare sliderX.
  • Găsiți punctul de atingere de origine.
  • Ascultați următorul touchmove pentru a vedea dacă utilizatorul trage vertical sau orizontal.

După document.addEventListener, adăuga:

dacă (acțiune) action.stop ();

Acest lucru va opri orice alte acțiuni (cum ar fi parcurgerea momentei pe care o vom implementa în fizică stopTouchScroll) de la deplasarea cursorului. Acest lucru va permite utilizatorului să "captureze" glisorul imediat dacă trece printr-un element sau titlu pe care dorește să facă clic.

Apoi, trebuie să stocăm punctul de contact de origine. Acest lucru ne va permite să vedem în ce direcție utilizatorul își mișcă degetul în continuare. Dacă este o mișcare verticală, vom permite derularea paginii ca de obicei. Dacă este o mișcare orizontală, vom deplasa în schimb sliderul.

Vrem să împărtășim asta touchOrigin între operatorii de evenimente. Deci dupa permiteți acțiune; adăuga:

permiteți touchOrigin = ;

Înapoi în nostru startTouchScroll handler, adăugați:

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

Acum putem adăuga a touchmove ascultător de evenimente la document pentru a determina direcția de tragere bazată pe aceasta touchOrigin:

document.addEventListener ('touchmove', determineDragDirection);

Al nostru determineDragDirection funcția va măsura următoarea locație a atingerii, va verifica dacă a fost mutată și, dacă este cazul, va măsura unghiul pentru a determina dacă este verticală sau orizontală:

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

Popmotion include câteva calculatoare utile pentru măsurarea distanțelor dintre două coordonate x / y. Putem importa astfel:

const calc, css, valoare = window.popmotion;

Apoi, măsurarea distanței dintre cele două puncte este o chestiune de utilizare a distanţă calculator:

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

Acum, dacă atingerea sa mișcat, putem dezactiva acest ascultător al evenimentului.

dacă (! distanța) retur; document.removeEventListener ('touchmove', determineDragDirection);

Măsurați unghiul dintre cele două puncte cu unghi calculator:

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

Putem folosi aceasta pentru a determina dacă acest unghi este un unghi orizontal sau vertical, prin trecerea acestuia la următoarea funcție. Adăugați această funcție la începutul fișierului nostru:

funcția angleIsVertical (unghiul) const esteUp = (unghiul <= -90 + 45 && angle >= -90 - 45); const esteDown = (unghiul <= 90 + 45 && angle >= 90 - 45); retur (isUp || isDown); 

Această funcție revine Adevărat dacă unghiul prevăzut este în intervalul -90 +/- 45 grade (drept în sus) sau 90 +/- 45 de grade (drept în jos.) Deci, putem adăuga un alt întoarcere dacă această funcție revine Adevărat.

dacă (return angleUsVertical (unghi));

Urmărirea indicatorilor

Acum știm că utilizatorul încearcă să deruleze caruselul, putem începe să urmărim degetul. Popmotion oferă o acțiune indicatoare care va afișa coordonatele x / y ale mouse-ului sau indicatorul de atingere.

Mai întâi, importă ac indicator:

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

Pentru a urmări intrarea touch, furnizați evenimentul inițial ac indicator:

action = pointer (e) .start ();

Vrem să măsuram inițial X poziționați indicatorul nostru și aplicați orice mișcare spre cursor. Pentru asta, putem folosi un transformator numit applyOffset.

Transformatoarele sunt funcții pure care iau o valoare și o returnează - da-transformată. De exemplu: const dublu = (v) => v * 2.

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

applyOffset este o funcție curried. Aceasta înseamnă că atunci când o numim, ea creează o nouă funcție care poate fi apoi trecută printr-o valoare. Mai întâi o numim cu un număr pe care dorim să îl măsuram, în acest caz valoarea curentă action.x, și un număr pentru a aplica acea compensare. În acest caz, asta e al nostru sliderX.

Deci, noi applyOffset funcția va arăta astfel:

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

Acum putem folosi această funcție în pointer producție apel invers pentru a aplica cursorul mișcării către cursor.

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

Oprirea, cu stil

Caruselul este acum trasabil prin atingere! Puteți testa acest lucru utilizând emularea de dispozitive în Instrumentele de dezvoltare ale Chrome.

Se simte putin janky, nu? S-ar putea să fi întâmpinat defilare care se simte așa: Îți ridici degetul și scrollingul se oprește. Sau scrollingul se oprește și apoi o mică animație preia falsul o continuare a derulării.

Nu o să facem asta. Putem folosi acțiunea fizică din Popmotion pentru a lua viteza adevărată sliderX și aplicați frecare acestuia pe o perioadă de timp.

În primul rând, adăugați-l la lista noastră tot mai mare de importuri:

const calc, css, fizică, pointer, valoare = window.popmotion;

Apoi, la sfârșitul nostru stopTouchScroll adăugați:

dacă (acțiune) action.stop (); actiune = fizica (din: sliderX.get (), viteza: sliderX.getVelocity (), frecare: 0.2) .output ((v) => sliderX.set (v)).

Aici, din și viteză sunt setate cu valoarea curentă și viteza de sliderX. Acest lucru ne asigură că simularea fizică are aceleași condiții inițiale ca și mișcarea de tragere a utilizatorului.

frecare este setat ca 0.2. Fricțiunea este setată ca o valoare de la 0 la 1, cu 0 fără să fie deloc fricțiune și 1 fiind frecare absolută. Încercați să jucați cu această valoare pentru a vedea schimbarea pe care o face la "sentimentul" caruselului atunci când un utilizator nu mai trage.

Numerele mai mici vor face să se simtă mai ușoare, iar numerele mai mari vor face mișcarea mai grea. Pentru o mișcare de defilare, simt 0.2 lovește un echilibru frumos între erratic și lent.

graniţele

Dar există o problemă! Dacă ați jucat cu noul carusel tactil, este evident. Nu am limitat mișcarea, făcând posibilă aruncarea literală a caruselului!

Există un alt transformator pentru această slujbă, clemă. Aceasta este, de asemenea, o funcție curried, adică dacă o numim cu o valoare min și max, să spunem 0 și 1, aceasta va reveni la o nouă funcție. În acest exemplu, noua funcție va restricționa orice număr dat între el 0 și 1:

clemă (0, 1) (5); // returnează 1

Mai întâi, importă clemă:

const applyOffset, clamp = transformare;

Vrem să folosim această funcție de strângere pe caruselul nostru, deci adăugați această linie după ce definim minXOffset:

const clampXOffset = clemă (minXOffset, maxXOffset);

Vom modifica cele două producție am stabilit acțiunile noastre folosind o compoziție ușor funcțională cu țeavă transformator.

țeavă

Când numim o funcție, scriem astfel:

foo (0);

Dacă vrem să dăm rezultatul acelei funcții unei alte funcții, am putea scrie așa:

bar (foo (0));

Acest lucru devine ușor de citit și se agravează doar pe măsură ce adăugăm tot mai multe funcții.

Cu țeavă, putem compune o nouă funcție din foo și bar pe care le putem reutiliza:

const foobar = țeava (foo, bar); foobar (0);

Este, de asemenea, scris într-un început natural -> ordine de terminare, ceea ce face mai ușor de urmat. Putem folosi asta pentru a compune applyOffset și clemă într-o singură funcție. Import țeavă:

const applyOffset, clemă, țeavă = transformă;

Inlocuieste producție apel invers al nostru ac indicator cu:

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

Și înlocuiți producție apel invers fizică cu:

țeavă (clampXOffset, (v) => sliderX.set (v))

Acest tip de compoziție funcțională poate crea cu ușurință procese descriptive, pas cu pas, din funcții mai mici, reutilizabile.

Acum, când trageți și aruncați caruselul, nu se va mișca în afara limitelor sale.

Oprirea bruscă nu este foarte satisfăcătoare. Dar aceasta este o problemă pentru o parte ulterioară!

Concluzie

Asta-i totul pentru partea 1. Până acum, am analizat carusele existente pentru a vedea punctele forte și punctele slabe ale diferitelor abordări de derulare. Am folosit metoda de urmărire a datelor și fizică de la Popmotion pentru a anima performanțele caruselului nostru translateX cu deplasare tactilă. Am fost, de asemenea, introduși în compoziția funcțională și funcțiile curried.

Puteți lua o versiune comentată a "povestii de până acum" pe acest CodePen.

În următoarele rate, vom analiza:

  • derularea cu o rotiță a mouse-ului
  • reevaluarea caruselului atunci când fereastra se redimensionează
  • paginare, cu accesibilitate pentru tastatură și mouse
  • priviri delicioase, cu ajutorul fizicii de primăvară

Așteaptă cu nerăbdare să te văd acolo!

Cod