Maak de perfecte carrousel, deel 1

Carrousels zijn een nietje van streaming- en e-commercesites. Zowel Amazon als Netflix gebruiken ze als prominente navigatiehulpmiddelen. In deze zelfstudie evalueren we het interactieontwerp van beide en gebruiken we onze bevindingen om de perfecte carrousel te implementeren.

In deze tutorialserie leren we ook enkele functies van Popmotion, een JavaScript-bewegingsengine. Het biedt animatietools zoals tweens (nuttig voor paginering), aanwijzer volgen (om te scrollen) en lente-fysica (voor onze prachtige afwerking.)

Deel 1 zal evalueren hoe Amazon en Netflix scrollen hebben geïmplementeerd. Vervolgens implementeren we een carrousel die via aanraken kan worden geschoven.

Aan het einde van deze serie hebben we wiel- en touchpad-scroll, paginering, voortgangsbalken, toetsenbordnavigatie en enkele kleine details met behulp van veerfysica geïmplementeerd. We zullen ook zijn blootgesteld aan een aantal functionele basissamenstellingen.

Perfect?

Wat is ervoor nodig om een ​​carrousel "perfect" te laten zijn? Het moet toegankelijk zijn door:

  • Muis: Het zou eerdere en volgende knoppen moeten bieden die gemakkelijk te klikken zijn en de inhoud niet verbergen.
  • Touch: Het moet de vinger volgen en vervolgens met hetzelfde momentum scrollen als wanneer de vinger van het scherm omhoog komt.
  • Muis wieltje: Vaak over het hoofd gezien, bieden de Apple Magic Mouse en vele laptop trackpads soepel horizontaal scrollen. We zouden die mogelijkheden moeten gebruiken!
  • Toetsenbord: Veel gebruikers geven de voorkeur aan, of kunnen geen muis gebruiken voor navigatie. Het is belangrijk dat we onze carrousel toegankelijk maken, zodat deze gebruikers ons product ook kunnen gebruiken.

Tot slot gaan we de dingen nog een stapje verder en maken dit een zelfverzekerd, heerlijk stukje UX door de carrousel duidelijk en visceraal te laten reageren met veerfysica wanneer de schuifregelaar het einde heeft bereikt.

Opstelling

Laten we eerst de HTML en CSS krijgen die nodig zijn om een ​​rudimentaire carrousel te bouwen door deze CodePen te beschrijven. 

De Pen is opgezet met Sass voor het voorbewerken van CSS en Babel voor het transponeren van ES6 JavaScript. Ik heb ook Popmotion meegeleverd, waartoe je toegang hebt window.popmotion.

U kunt de code kopiëren naar een lokaal project als u dat wilt, maar u moet ervoor zorgen dat uw omgeving Sass en ES6 ondersteunt. Je moet ook Popmotion installeren met npm installeer popmotion.

Een nieuwe carrousel maken

Op elke pagina kunnen we veel carrousels hebben. We hebben dus een methode nodig om de status en functionaliteit van elk in te kapselen.

Ik ga een gebruiken fabrieksfunctie liever dan een klasse. Fabrieksfuncties vermijden de noodzaak om het vaak verwarrende te gebruiken deze sleutelwoord en vereenvoudigt de code voor de doeleinden van deze tutorial.

Voeg in je JavaScript-editor deze eenvoudige functie toe:

functie carrousel (container)  carrousel (document.querySelector ('. container'));

We voegen hier onze carrouselspecifieke code toe carrousel functie.

De Hows en Whys van scrollen

Onze eerste taak is om de carrousel te laten rollen. Er zijn twee manieren om dit aan te pakken:

Native Browser Scrolling

De voor de hand liggende oplossing zou zijn om in te stellen overflow-x: scrollen op de schuifregelaar. Dit zou native scrollen in alle browsers mogelijk maken, inclusief aanraak- en horizontale muiswielinrichtingen.

Er zijn echter nadelen aan deze aanpak:

  • Inhoud buiten de container zou niet zichtbaar zijn, wat beperkend kan zijn voor ons ontwerp.
  • Het beperkt ook de manieren waarop we animaties kunnen gebruiken om aan te geven dat we het einde hebben bereikt.
  • Desktopbrowsers hebben een lelijke (hoewel toegankelijk!) Horizontale schuifbalk.

Alternatief:

bezielen translateX

We kunnen ook de carrousels animeren translateX eigendom. Dit zou zeer veelzijdig zijn omdat we precies het ontwerp kunnen implementeren dat we leuk vinden. translateX is ook erg performant, in tegenstelling tot de CSS links eigenschap kan deze worden afgehandeld door de GPU van het apparaat.

Nadeel is dat we de scrolfunctionaliteit opnieuw moeten toepassen met behulp van JavaScript. Dat is meer werk, meer code.

Hoe gaan benaderingen met Amazon en Netflix scrollen?

Zowel Amazon en Netflix carrousels maken verschillende compromissen bij het benaderen van dit probleem.

Amazone bezielt de carrousels links eigenschap in de "bureaublad" -modus. animeren links is een ongelooflijk slechte keuze, omdat het wijzigen ervan een herberekening van de lay-out triggert. Dit is CPU-intensief en oudere machines zullen moeite hebben om 60 fps te raken.

Degene die de beslissing heeft genomen om te bewegen links in plaats van translateX moet een echte idioot zijn (spoiler: Ik was het, terug in 2012. We waren niet zo verlicht in die dagen.)

Wanneer het carrousel een aanraakapparaat detecteert, gebruikt het de oorspronkelijke scrollfunctie van de browser. Het probleem met alleen inschakelen in de modus "mobiel" is dat desktopgebruikers met horizontale scrollwielen missen. Het betekent ook dat elke inhoud buiten de carrousel visueel moet worden afgesneden:

Netflix correct animeert de carrousels translateX eigendom, en dit gebeurt op alle apparaten. Hierdoor kunnen ze een ontwerp hebben dat bloedt buiten de carrousel:

Hierdoor kunnen ze op hun beurt een chique ontwerp maken waarbij items buiten de x- en y-randen van de carrousel worden vergroot en de omliggende items uit de weg gaan:

Helaas is de herimplementatie van het scrollen door Netflix voor aanraakapparaten onbevredigend: het gebruikt een op bewegingen gebaseerd paginatiesysteem dat langzaam en omslachtig aanvoelt. Er is ook geen aandacht voor horizontale scrollwielen.

We kunnen het beter doen. Laten we coderen!

Scrollend als een professional

Onze eerste zet is om de .schuif knooppunt. Terwijl we bezig zijn, nemen we de items die het bevat, zodat we de dimensie van de schuifregelaar kunnen achterhalen.

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

De carrousel meten

We kunnen het zichtbare gedeelte van de schuif bepalen door de breedte te meten:

const sliderVisibleWidth = slider.offsetWidth;

We willen ook de totale breedte van alle items die erin zijn opgenomen. Om onze te houden carrousel functioneer relatief schoon, laten we deze berekening in een aparte functie boven in ons bestand zetten.

Door het gebruiken van getBoundingClientRect om het te meten links offset van ons eerste item en de rechts offset van ons laatste item, kunnen we het verschil tussen hen gebruiken om de totale breedte van alle items te vinden.

function getTotalItemsWidth (items) const left = items [0] .getBoundingClientRect (); const right = items [items.length - 1] .getBoundingClientRect (); keer terug rechts - links; 

Na onze sliderVisibleWidth meting, schrijf:

const totalItemsWidth = getTotalItemsWidth (items);

We kunnen nu de maximale afstand berekenen die onze carrousel moet kunnen schuiven. Het is de totale breedte van al onze items, min één volle breedte van onze zichtbare schuifregelaar. Dit biedt een nummer waarmee het meest rechtse item kan worden uitgelijnd met het recht van onze schuifregelaar:

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

Met deze metingen op zijn plaats, zijn we klaar om te beginnen met het scrollen van onze carrousel.

omgeving translateX

Popmotion wordt geleverd met een CSS-renderer voor de eenvoudige en performante instelling van CSS-eigenschappen. Het komt ook met een waardefunctie die kan worden gebruikt om nummers bij te houden en, belangrijker (zoals we snel zullen zien), om hun snelheid op te vragen.

Bovenaan je JavaScript-bestand importeer je ze als volgt:

const css, value = window.popmotion;

Daarna, op de lijn nadat we begonnen minXOffset, maak een CSS-renderer aan voor onze slider:

const sliderRenderer = css (slider);

En maak een waarde om de x-offset van onze schuifregelaar bij te houden en de schuifregelaars bij te werken translateX eigendom wanneer het verandert:

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

Het verplaatsen van de schuifregelaar is nu net zo eenvoudig als schrijven:

sliderX.set (-100);

Probeer het!

Tik op Scrollen

We willen dat onze carrousel begint te scrollen wanneer een gebruikersleept de schuifregelaar horizontaal en stopt met scrollen wanneer een gebruiker het scherm niet meer aanraakt. Onze event-handlers zien er als volgt uit:

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

In onze startTouchScroll functie, willen we:

  • Stop met andere acties die aanzetten sliderX.
  • Zoek het beginpunt van de oorsprong.
  • Luister naar de volgende touchmove gebeurtenis om te zien of de gebruiker verticaal of horizontaal sleept.

Na document.addEventListener, toevoegen:

als (actie) action.stop ();

Dit stopt alle andere acties (zoals de door fysica aangedreven momentum scroll die we zullen implementeren stopTouchScroll) om de schuifregelaar te verplaatsen. Hierdoor kan de gebruiker de schuifregelaar onmiddellijk "vangen" als deze voorbij een item of titel schuift waarop ze willen klikken.

Vervolgens moeten we het beginpunt van de oorsprong opslaan. Op die manier kunnen we zien waar de gebruiker zijn vinger naast zet. Als het een verticale beweging is, laten we het scrollen van de pagina zoals gebruikelijk. Als het een horizontale beweging is, scrollen we in plaats daarvan door de schuifregelaar.

We willen dit delen touchOrigin tussen event handlers. Dus na laat actie; toevoegen:

laat touchOrigin = ;

Terug in onze startTouchScroll handler, voeg toe:

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

We kunnen nu een toevoegen touchmove gebeurtenis luisteraar voor de document om de draairichting op basis hiervan te bepalen touchOrigin:

document.addEventListener ('touchmove', bepaalDragDirection);

Onze determineDragDirection functie gaat de volgende aanraaklocatie meten, controleren of deze daadwerkelijk is verplaatst en, zo ja, de hoek meten om te bepalen of deze verticaal of horizontaal is:

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

Popmotion bevat een aantal handige calculators voor het meten van dingen zoals de afstand tussen twee x / y-coördinaten. We kunnen die als volgt importeren:

const calc, css, value = window.popmotion;

Het meten van de afstand tussen de twee punten is een kwestie van het gebruiken van de afstand rekenmachine:

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

Als de aanraking is verplaatst, kunnen we deze gebeurtenislistener uitschakelen.

als (! afstand) terugkomt; document.removeEventListener ('touchmove', bepaalDragDirection);

Meet de hoek tussen de twee punten met de hoek rekenmachine:

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

We kunnen dit gebruiken om te bepalen of deze hoek een horizontale of verticale hoek is, door deze door te geven aan de volgende functie. Voeg deze functie toe aan de top van ons bestand:

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

Deze functie retourneert waar als de geleverde hoek binnen -90 +/- 45 graden (recht omhoog) of 90 +/- 45 graden (recht naar beneden) ligt. We kunnen dus een andere hoek toevoegen terugkeer clausule als deze functie terugkeert waar.

als (angleIsVertical (hoek)) terugkomt;

Aanwijzer volgen

Nu weten we dat de gebruiker probeert door de carrousel te bladeren, we kunnen beginnen met het volgen van zijn vinger. Popmotion biedt een aanwijzeractie die de x / y-coördinaten van een muis of aanraakaanwijzer uitvoert.

Eerst importeren wijzer:

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

Om de aanraakinvoer te volgen, geeft u de oorspronkelijke gebeurtenis aan wijzer:

action = pointer (e) .start ();

We willen de initiaal meten X positie van onze aanwijzer en past elke beweging op de schuifregelaar toe. Daarvoor kunnen we een transformator gebruiken genaamd applyOffset.

Transformers zijn pure functies die een waarde aannemen en deze teruggeven - ja-getransformeerd. Bijvoorbeeld: const double = (v) => v * 2.

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

applyOffset is een curried-functie. Dit betekent dat wanneer we het noemen, het een nieuwe functie creëert die vervolgens een waarde kan worden doorgegeven. We noemen het eerst met een nummer waarvan we de offset willen meten, in dit geval de huidige waarde van action.x, en een nummer om die offset toe te passen. In dit geval is dat onze sliderX.

Zo onze applyOffset functie ziet er als volgt uit:

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

We kunnen deze functie nu in de aanwijzer gebruiken uitgang terugbellen om wijzerbeweging toe te passen op de schuifregelaar.

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

Stoppen, met stijl

De carrousel kan nu worden versleept door aanraking! U kunt dit testen door apparaatemulatie te gebruiken in de ontwikkelaarstools van Chrome.

Het voelt een beetje janky, toch? Mogelijk bent u scrollen tegengekomen dat zich eerder zo heeft gevoeld: u heft uw vinger op en het scrollen stopt. Of het scrollen stopt dood en vervolgens neemt een kleine animatie het voortzetting van het scrollen over.

Dat gaan we niet doen. We kunnen de natuurkundige actie in Popmotion gebruiken om de ware snelheid van te nemen sliderX en wrijving toepassen op het gedurende een tijdsduur.

Voeg het eerst toe aan onze steeds groter wordende lijst van importen:

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

Vervolgens, aan het einde van onze stopTouchScroll functie, voeg toe:

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

Hier, van en snelheid worden ingesteld met de huidige waarde en snelheid van sliderX. Dit zorgt ervoor dat onze simulatie van fysica dezelfde initiële startvoorwaarden heeft als de sleepbeweging van de gebruiker.

wrijving wordt ingesteld als 0.2. Wrijving wordt ingesteld als een waarde van 0 naar 1, met 0 helemaal geen wrijving zijn en 1 absoluut wrijving zijn. Probeer eens rond te spelen met deze waarde om de verandering te zien die het maakt naar het "gevoel" van de carrousel wanneer een gebruiker stopt met slepen.

Kleinere nummers zullen het lichter laten aanvoelen, en grotere aantallen zullen beweging zwaarder maken. Voor een rollende beweging voel ik 0.2 heeft een mooie balans tussen grillig en traag.

grenzen

Maar er is een probleem! Als je met je nieuwe aanraakcarrousel hebt gespeeld, is dat duidelijk. We hebben geen beweging begrensd, waardoor het mogelijk is om je carrousel letterlijk weg te gooien!

Er is nog een transformator voor deze klus, klem. Dit is ook een curried-functie, wat betekent dat als we het een min- en max-waarde noemen, zeggen 0 en 1, het zal een nieuwe functie teruggeven. In dit voorbeeld beperkt de nieuwe functie elk gegeven getal tot tussen 0 en 1:

klem (0, 1) (5); // geeft als resultaat 1

Eerst importeren klem:

const applyOffset, clamp = transformeren;

We willen deze klemfunctie gebruiken in onze carrousel, dus voeg deze regel toe nadat we deze hebben gedefinieerd minXOffset:

const clampXOffset = clamp (minXOffset, maxXOffset);

We gaan de twee wijzigen uitgang we hebben onze acties opgezet met behulp van een lichte functionele compositie bij de pijp transformator.

Pijp

Wanneer we een functie noemen, schrijven we het als volgt:

foo (0);

Als we de uitvoer van die functie aan een andere functie willen geven, kunnen we dat zo schrijven:

bar (foo (0));

Dit wordt enigszins moeilijk te lezen en het wordt alleen maar erger naarmate we meer en meer functies toevoegen.

Met pijp, we kunnen een nieuwe functie samenstellen uit foo en bar welke we kunnen hergebruiken:

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

Het is ook geschreven in een natuurlijke start -> afwerkingvolgorde, waardoor het gemakkelijker te volgen is. We kunnen dit gebruiken om te componeren applyOffset en klem in een enkele functie. Importeren pijp:

const applyOffset, clamp, pipe = transform;

Vervang de uitgang terugbellen van onze wijzer met:

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

En vervang de uitgang terugbellen van fysica met:

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

Dit soort functionele samenstelling kan vrij beschrijvende, stapsgewijze processen creëren uit kleinere, herbruikbare functies.

Als u de carrousel sleept en weggooit, wijkt deze niet buiten de grenzen.

De abrupte stop is niet erg bevredigend. Maar dat is een probleem voor een later deel!

Conclusie

Dat is alles voor deel 1. Tot nu toe hebben we een kijkje genomen bij bestaande carrousels om de sterke en zwakke punten van verschillende benaderingen van scrollen te zien. We hebben de input-tracking en physics van Popmotion gebruikt om onze carrousels op een performante manier te animeren translateX met touch-scrolling. We hebben ook kennis gemaakt met functionele compositie en gecaste functies.

Je kunt een commentaarversie van het "verhaal tot nu toe" op deze CodePen nemen.

In aankomende termijnen zullen we kijken naar:

  • scrollen met een muiswiel
  • het opnieuw meten van de carrousel wanneer het venster wordt verkleind
  • paginering, met toegankelijkheid voor toetsenbord en muis
  • verrukkelijke details, met behulp van de lentefysica

Ik kijk ernaar uit je daar te zien!