Maak de perfecte carrousel, deel 2

Welkom terug bij de Create the Perfect Carousel-lessenreeks. We maken een toegankelijke en heerlijke carrousel met behulp van de fysica, tween en input-trackingmogelijkheden van JavaScript en Popmotion.

In deel 1 van onze tutorial hebben we bekeken hoe Amazon en Netflix hun carrousels hebben gemaakt en de voor- en nadelen van hun aanpak hebben geëvalueerd. Met onze kennis hebben we een strategie voor onze carrousel gekozen en touch-scrolling geïmplementeerd met behulp van fysica.

In deel 2 gaan we horizontale muisschuiven implementeren. We gaan ook kijken naar enkele algemene pagineringstechnieken en deze implementeren. Ten slotte gaan we een voortgangsbalk aansluiten die aangeeft hoe ver door de carrousel de gebruiker zich bevindt.

U kunt uw opslagpunt herstellen door deze CodePen te openen, die verder gaat waar we zijn gebleven.

Horizontaal muis scrollen

Het komt zelden voor dat een JavaScript-carrousel de horizontale muisbeweging respecteert. Dit is een schande: op laptops en muizen die op het momentum gebaseerd horizontaal scrollen implementeren, is dit verreweg de snelste manier om door de carrousel te navigeren. Het is net zo erg als aanraakgebruikers te dwingen via knoppen te navigeren in plaats van te vegen.

Gelukkig kan het in slechts een paar regels code worden geïmplementeerd. Aan het einde van uw carrousel functie, voeg een nieuwe gebeurtenislistener toe:

container.addEventListener ('wheel', onWheel);

Onder jouw startTouchScroll gebeurtenis, voeg een stub-functie toe genaamd onWheel:

function onWheel (e) console.log (e.deltaX)

Als u nu het scrollwiel over de carrousel beweegt en uw consolepaneel controleert, ziet u de wielafstand op de x-asuitvoer.

Net als bij aanraken, als de wielbeweging grotendeels verticaal is, zou de pagina zoals gewoonlijk moeten schuiven. Als het horizontaal is, willen we de wielbeweging vastleggen en toepassen op de carrousel. Dus, in onWheel, vervang de console.log met:

const angle = calc.angle (x: e.deltaX, y: e.deltaY); als (angleIsVertical (hoek)) terugkomt; e.stopPropagation (); e.preventDefault ();

Dit codeblok stopt het bladeren van de pagina als de schuif horizontaal is. Het bijwerken van de x-offset van onze schuifregelaar is nu slechts een kwestie van het nemen van de evenementen deltaX eigendom en dat toe te voegen aan onze huidige sliderX waarde:

const newX = clampXOffset (sliderX.get () + - e.deltaX); sliderX.set (kunnen we nieuwe);

We hergebruiken onze vorige clampXOffset functie om deze berekening in te pakken en ervoor te zorgen dat de carrousel niet voorbij zijn gemeten grenzen schuift.

Een beetje op Throttling Scroll Events

Elke goede tutorial die zich bezighoudt met input-evenementen zal uitleggen hoe belangrijk het is om die events te vertragen. Dit komt omdat scrollen, muizen en aanraakgebeurtenissen allemaal sneller kunnen worden geactiveerd dan de framesnelheid van het apparaat.

U wilt geen onnodig arbeidsintensief werk doen, zoals de carrousel twee keer in één frame weergeven, want het is verspilling van middelen en een snelle manier om een ​​trage interface te creëren.

Deze tutorial is daar niet op ingegaan, omdat de door Popmotion geleverde renderers Framesync implementeren, een piepkleine frame-gesynchroniseerde taakplanner. Dit betekent dat je zou kunnen bellen (v) => sliderRenderer.set ('x', v) meerdere keren achter elkaar, en de dure weergave zou slechts één keer gebeuren, in het volgende frame.

Paginering

Dat is scrollen voltooid. Nu moeten we wat leven inblazen in de tot nu toe onbemiddelde navigatieknoppen.

Nu gaat deze tutorial over interactie, dus voel je vrij om deze knoppen naar eigen inzicht in te richten. Persoonlijk vind ik richtingspijlen intuïtiever (en standaard volledig internationalized!).

Hoe moet paginering werken?

Er zijn twee duidelijke strategieën die we zouden kunnen nemen bij het pagineren van de carrousel: punt per punt of eerste verduisterde item. Er is maar één juiste strategie, maar omdat ik de andere zo vaak heb zien implementeren, dacht ik dat het de moeite waard was om het uit te leggen waarom het is onjuist.

1. Item voor item

Meet eenvoudig de x-afstand van het volgende item in de lijst en animeer het schap met dat aantal. Het is een heel eenvoudig algoritme waarvan ik aanneem dat het wordt gekozen vanwege de eenvoud in plaats van de gebruiksvriendelijkheid.

Het probleem is dat op de meeste schermen veel items tegelijkertijd kunnen worden getoond en dat mensen ze allemaal zullen scannen voordat ze proberen te navigeren.

Het voelt traag, zo niet helemaal frustrerend. De enige situatie waarin dit een goede keuze zou zijn, is als u weten de items in je carrousel hebben dezelfde breedte of zijn slechts iets kleiner dan het zichtbare gedeelte.

Als we echter naar meerdere items kijken, kunnen we beter de methode voor het eerste verborgen object gebruiken:

2. Eerste verduisterde item

Deze methode zoekt eenvoudigweg naar de eerste verduisterde item in de richting waarin we de carrousel willen verplaatsen, neemt het zijn x offset, en scrollt er vervolgens naar.

Daarbij halen we het maximale aantal nieuwe items op in de veronderstelling dat de gebruiker alle aanwezigen heeft gezien.

Omdat we meer items binnenhalen, heeft de carrousel minder klikken nodig om te navigeren. Snellere navigatie vergroot de betrokkenheid en zorgt ervoor dat uw gebruikers meer van uw producten te zien krijgen.

Evenement Luisteraars

Laten we eerst onze gebeurtenislisteners instellen, zodat we kunnen beginnen met spelen met de paginering.

We moeten eerst onze vorige en volgende knoppen selecteren. Bij de top van de carrousel functie, voeg toe:

const nextButton = container.querySelector ('. volgende'); const prevButton = container.querySelector ('. vorige');

Vervolgens, bij de bodem van de carrousel functie, voeg de gebeurtenislisteners toe:

nextButton.addEventListener ('click', gotoNext); prevButton.addEventListener ('click', gotoPrev);

Eindelijk, net boven je blok met gebeurtenislisteners, voeg je de eigenlijke functies toe:

functie goto (delta)  const gotoNext = () => ga naar (1); const gotoPrev = () => goto (-1);

ga naar is de functie die alle logica voor paginering zal behandelen. Er is gewoon een nummer voor nodig dat de reisrichting aangeeft die we willen pagineren. gotoNext en gotoPrev gewoon deze functie aanroepen met 1 of -1, respectievelijk.

Een "pagina" berekenen

Een gebruiker kan vrijelijk door deze carrousel scrollen en dat is zo n items erin en de grootte van de carrousel kan worden gewijzigd. Dus het concept van een traditionele pagina is hier niet direct van toepassing. We tellen het aantal pagina's niet mee.

In plaats daarvan, wanneer de ga naar functie wordt genoemd, we gaan in de richting kijken van delta en zoek het eerste gedeeltelijk verduisterde item. Dat wordt het eerste item op onze volgende "pagina".

De eerste stap is om de huidige x-offset van onze schuifregelaar te krijgen en die met de volledige zichtbare breedte van de schuifregelaar te gebruiken om een ​​"ideale" offset te berekenen waarnaar we willen schuiven. De ideale offset is wat wij zou scrol naar als we naïef waren naar de inhoud van de schuifregelaar. Het is een goede plek om naar ons eerste item te zoeken.

const currentX = sliderX.get (); laat targetX = currentX + (- sliderVisibleWidth * delta);

We kunnen hier een brutale optimalisatie gebruiken. Door onze TargetX naar de clampXOffset functie die we in de vorige tutorial hebben gemaakt, kunnen we zien of de uitvoer anders is TargetX. Als dat zo is, betekent het onze TargetX valt buiten onze schuifbare grenzen, dus we hoeven niet het dichtstbijzijnde item te vinden. We scrollen gewoon naar het einde.

const clampedX = clampXOffset (targetX); targetX = (targetX === clampedX)? findClosestItemOffset (targetX, delta): clampedX;

Het dichtstbijzijnde item vinden

Het is belangrijk op te merken dat de volgende code werkt in de veronderstelling dat alle items in uw carrousel hebben dezelfde afmetingen. In die veronderstelling kunnen we optimalisaties maken, zoals het niet hoeven meten van de grootte van elk item. Als je items zijn verschillende maten, dit zal nog steeds een goed startpunt zijn. 

Boven je ga naar functie, voeg de findClosestItemOffset functie waarnaar wordt verwezen in het laatste fragment:

function findClosestItem (targetX, delta) 

Ten eerste moeten we weten hoe breed onze items zijn en hoe groot ze zijn. De Element.getBoundingClientRect () methode kan alle informatie bieden die we nodig hebben. Voor breedte, we meten eenvoudig het eerste itemelement. Om de afstand tussen items te berekenen, kunnen we de rechts offset van het eerste item en het links offset van de tweede, en trek vervolgens de eerste af van de laatste: 

const right, width = items [0] .getBoundingClientRect (); const spacing = items [1] .getBoundingClientRect (). left - right;

Nu, met de TargetX en delta variabelen die we hebben doorgegeven aan de functie, we hebben alle gegevens die we nodig hebben om snel een offset te berekenen waarnaar we kunnen scrollen.

De berekening is om het absolute te verdelen TargetX waarde door de breedte + spatiëring. Dit geeft ons het exacte aantal items dat we binnen die afstand passen.

const totalItems = Math.abs (targetX) / (width + spacing);

Vervolgens, naar boven of naar beneden, afhankelijk van de richting van paginering (onze delta). Dit geeft ons het aantal compleet items die we kunnen passen.

const totalCompleteItems = delta === 1? Math.floor (totalItems): Math.ceil (totalItems);

Tot slot, vermenigvuldig dat aantal met breedte + spatiëring om ons een offset flush te geven met een volledig item.

return 0 - totalCompleteItems * (width + spacing);

Animeer de paginering

Nu dat we onze hebben TargetX berekend, we kunnen er aan bezielen! Hiervoor gaan we het werkpaard van webanimatie gebruiken, de tween.

Voor niet-ingewijden is 'tween' een afkorting voor wordentween. Een tween verandert van de ene naar de andere waarde over een ingestelde tijdsduur. Als je CSS-overgangen hebt gebruikt, is dit hetzelfde. 

Er zijn een aantal voordelen (en tekortkomingen!) Bij het gebruik van JavaScript via CSS voor tweens. In dit geval, omdat we ook aan het animeren zijn sliderX met fysica en gebruikersinvoer is het voor ons gemakkelijker om in deze workflow voor de tween te blijven.

Het betekent ook dat we later een voortgangsbalk kunnen aansluiten en deze zal natuurlijk met al onze animaties werken, gratis.

We willen eerst importeren tween van Popmotion:

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

Aan het einde van onze ga naar functie, kunnen we onze tween toevoegen die animeert van currentX naar TargetX:

tween (from: currentX, to: targetX, onUpdate: sliderX). start ();

Standaard worden Popmotion-sets ingesteld looptijd naar 300 milliseconden en gemak naar easing.easeOut. Deze zijn speciaal gekozen om een ​​responsief gevoel te geven aan animaties die reageren op gebruikersinvoer, maar voel je vrij om te spelen en te kijken of je iets bedacht dat beter bij het gevoel van je merk past.

Voortgangsindicator

Het is handig voor gebruikers om een ​​indicatie te hebben over waar in de carrousel ze zich bevinden. Hiervoor kunnen we een voortgangsindicator aansluiten.

Uw voortgangsbalk kan op verschillende manieren worden gestileerd. Voor deze zelfstudie hebben we een gekleurde div gemaakt, 5px hoog, die tussen de vorige en volgende knoppen loopt. Het is de manier waarop we dit koppelen aan onze code en de bar animeren die belangrijk is en de focus is van deze tutorial.

Je hebt de indicator nog niet gezien omdat we deze oorspronkelijk hebben gestileerd transformeren: scaleX (0). We gebruiken een schaal transformeren om de breedte van de balk aan te passen, omdat, zoals we in deel 1 hebben uitgelegd, transformaties meer performant zijn dan het veranderen van eigenschappen zoals links of, in dit geval, breedte.

Het stelt ons ook in staat om gemakkelijk code te schrijven die de schaal als een zet percentage: de huidige waarde van sliderX tussen minXOffset en maxXOffset.

Laten we beginnen met het selecteren van onze div.progress-bar na onze previousButton selector:

const progressBar = container.querySelector ('. voortgangsbalk');

Nadat we het hebben gedefinieerd sliderRenderer, we kunnen een renderer toevoegen voor voortgangsbalk:

const progressBarRenderer = css (progressBar);

Laten we nu een functie definiëren om het te updaten scaleX van de voortgangsbalk.

We gebruiken een calc functie genoemd getProgressFromValue. Dit heeft een bereik nodig, in ons geval min en maxXOffset, en een derde nummer. Het geeft het vooruitgang, een getal tussen 0 en 1, van dat derde getal binnen het opgegeven bereik.

functie updateProgressBar (x) const progress = calc.getProgressFromValue (maxXOffset, minXOffset, x); progressBarRenderer.set ('scaleX', voortgang); 

We hebben het bereik hier geschreven als maxXOffset, minXOffset wanneer, intuïtief, het moet worden omgekeerd. Dit is zo omdat X is een negatieve waarde, en maxXOffset is ook een negatieve waarde terwijl minXOffset is 0. De 0 is technisch gezien de grootste van de twee getallen, maar de kleinere waarde vertegenwoordigt eigenlijk de maximale offset. Minpunten, he?

We willen dat de voortgangsindicator in lockstep wordt bijgewerkt sliderX, dus laten we deze regel wijzigen:

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

Naar deze regel:

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

Nu, wanneer sliderX updates, net als de voortgangsbalk.

Conclusie

Dat is het voor deze aflevering! U kunt de nieuwste code op deze CodePen pakken. We hebben met succes horizontaal wielschuiven, paginering en een voortgangsbalk geïntroduceerd.

De carrousel is tot nu toe in vrij goede vorm! In de laatste tranche gaan we nog een stap verder. We zullen de carrousel volledig toetsenbord toegankelijk maken om ervoor te zorgen dat iedereen het kan gebruiken. 

We voegen ook een aantal heerlijke aanrakingen toe met een veerkrachtige ruk wanneer een gebruiker de carrousel langs zijn grenzen probeert te scrollen met scroll of paginering via aanraakbediening. 

Zie je dan!