Карусели являются одним из основных потоковых сайтов и сайтов электронной коммерции. И Amazon, и Netflix используют их как выдающиеся инструменты навигации. В этом уроке мы оценим дизайн взаимодействия обоих и используем наши выводы для реализации идеальной карусели.
В этой серии руководств мы также изучим некоторые функции движка JavaScript Popmotion . Он предлагает инструменты анимации, такие как анимация (полезная для разбиения на страницы), отслеживание указателя (для прокрутки) и физика пружины (для наших восхитительных завершающих штрихов.)
Часть 1 оценит, как Amazon и Netflix реализовали прокрутку. Затем мы реализуем карусель, которую можно прокручивать касанием.
К концу этой серии мы реализовали прокрутку с помощью колесика и сенсорной панели, нумерацию страниц, индикаторы выполнения, навигацию с помощью клавиатуры и некоторые небольшие штрихи с использованием пружинной физики. Мы также познакомились с некоторыми основными функциональными композициями.
Отлично?
Что нужно для того, чтобы карусель была «идеальной»? Он должен быть доступен для:
- Мышь: она должна предлагать предыдущую и следующую кнопки, которые легко нажимать и не затемнять содержимое.
- Прикосновение: он должен отслеживать палец, а затем прокручивать с тем же импульсом, что и при поднятии пальца с экрана.
- Колесо прокрутки: Часто забывают, что Apple Magic Mouse и многие трекпады для ноутбуков обеспечивают плавную горизонтальную прокрутку. Мы должны использовать эти возможности!
- Клавиатура: многие пользователи предпочитают не использовать мышь или не могут использовать ее для навигации. Важно, чтобы мы сделали нашу карусель доступной, чтобы эти пользователи тоже могли пользоваться нашим продуктом.
Наконец, мы сделаем еще один шаг вперед и сделаем этот уверенный, восхитительный кусок UX, заставляя карусель четко и визуально реагировать на пружинную физику, когда ползунок достиг конца.
Настроить
Во-первых, давайте получим HTML и CSS, необходимые для создания элементарной карусели, разветвив этот CodePen .
Pen использует Sass для предварительной обработки CSS и Babel для переноса ES6 JavaScript. Я также включил Popmotion, к которому можно получить доступ через window.popmotion
.
Вы можете скопировать код в локальный проект, если хотите, но вам нужно убедиться, что ваша среда поддерживает Sass и ES6. Вам также необходимо установить Popmotion с помощью npm install popmotion
.
Создание новой карусели
На любой странице у нас может быть много каруселей. Поэтому нам нужен метод для инкапсуляции состояния и функциональности каждого из них.
Я собираюсь использовать фабричную функцию, а не class
. Фабричные функции исключают необходимость использования часто вводящего в заблуждение this
ключевого слова и упростят код для целей данного руководства.
В вашем редакторе JavaScript добавьте эту простую функцию:
1
2
3
4
|
function carousel(container) {
}
carousel(document.querySelector(‘.container’));
|
Мы будем добавлять наш специфичный для карусели код внутри этой функции carousel
.
Как и почему прокрутка
Наша первая задача — сделать карусель прокрутки. Есть два способа сделать это:
Собственная прокрутка браузера
Очевидным решением было бы установить overflow-x: scroll
на слайдер. Это позволило бы прокручивать информацию во всех браузерах, включая сенсорные и горизонтальные устройства мыши.
Однако у этого подхода есть недостатки:
- Контент вне контейнера не будет виден, что может быть ограничением для нашего дизайна.
- Это также ограничивает способы, которыми мы можем использовать анимацию, чтобы указать, что мы достигли конца.
- Настольные браузеры будут иметь некрасивую (хотя и доступную!) Горизонтальную полосу прокрутки.
В качестве альтернативы:
Animate translateX
Мы также можем анимировать свойство translateX
карусели. Это было бы очень универсально, так как мы могли бы реализовать именно тот дизайн, который нам нравится. translateX
также очень производительный, так как в отличие от CSS-свойства left
он может обрабатываться графическим процессором устройства.
С другой стороны, нам пришлось бы переопределить функцию прокрутки с помощью JavaScript. Это больше работы, больше кода.
Как Amazon и Netflix подходят к прокрутке?
Карусели Amazon и Netflix делают разные компромиссы в решении этой проблемы.
Amazon анимирует left
свойство карусели в режиме «рабочего стола». Анимация left
— невероятно плохой выбор, так как его изменение вызывает пересчет макета . Это сильно загружает процессор, и на старых машинах будет сложно достичь скорости 60 кадров в секунду.
Тот, кто принял решение оживить left
вместо translateX
должен быть настоящим идиотом ( спойлер: это был я, еще в 2012 году. В те дни мы не были такими просвещенными).
При обнаружении сенсорного устройства карусель использует собственную прокрутку браузера. Проблема с включением только в «мобильном» режиме заключается в том, что пользователи настольных компьютеров с горизонтальными колесами прокрутки пропускают. Это также означает, что любой контент за пределами карусели должен быть визуально обрезан:
Netflix правильно анимирует свойство translateX
карусели, и это происходит на всех устройствах. Это позволяет им иметь дизайн, который кровоточит за пределами карусели:
Это, в свою очередь, позволяет им создать причудливый дизайн, в котором предметы увеличиваются за пределами краев x и y карусели, а окружающие предметы смещаются:
К сожалению, повторная реализация Netflix прокрутки для сенсорных устройств неудовлетворительна: она использует систему разбиения на страницы на основе жестов, которая кажется медленной и громоздкой. Там также не учитываются горизонтальные колеса прокрутки.
Мы можем сделать лучше. Давайте код!
Прокрутка как профессионал
Наш первый шаг — захватить узел .slider
. Пока мы на этом, давайте возьмем элементы, которые он содержит, чтобы мы могли выяснить размер слайдера.
1
2
3
4
|
function carousel(container) {
const slider = container.querySelector(‘.slider’);
const items = slider.querySelectorAll(‘.item’);
}
|
Измерение карусели
Мы можем определить видимую область слайдера, измерив его ширину:
1
|
const sliderVisibleWidth = slider.offsetWidth;
|
Нам также понадобится общая ширина всех элементов, содержащихся внутри. Чтобы сохранить нашу функцию carousel
относительно чистой, давайте поместим этот расчет в отдельную функцию вверху нашего файла.
Используя getBoundingClientRect
для измерения left
смещения нашего первого элемента и right
смещения нашего последнего элемента, мы можем использовать разницу между ними, чтобы найти общую ширину всех элементов.
1
2
3
4
5
|
function getTotalItemsWidth(items) {
const { left } = items[0].getBoundingClientRect();
const { right } = items[items.length — 1].getBoundingClientRect();
return right — left;
}
|
После нашего измерения sliderVisibleWidth
напишите:
1
|
const totalItemsWidth = getTotalItemsWidth(items);
|
Теперь мы можем определить максимальное расстояние, которое наша карусель должна прокручивать. Это общая ширина всех наших предметов, минус одна полная ширина нашего видимого слайдера. Это обеспечивает число, которое позволяет крайнему правому элементу выравниваться с правым ползунком:
1
2
|
const maxXOffset = 0;
const minXOffset = — (totalItemsWidth — sliderVisibleWidth);
|
После этих измерений мы готовы начать прокрутку нашей карусели.
Настройка translateX
Popmotion поставляется с рендером CSS для простой и производительной настройки свойств CSS. Он также поставляется с функцией значения, которую можно использовать для отслеживания чисел и, что важно (как мы скоро увидим), для запроса их скорости.
В верхней части вашего файла JavaScript импортируйте их следующим образом:
1
|
const { css, value } = window.popmotion;
|
Затем в строке после того, как мы установили minXOffset
, создайте CSS-рендеринг для нашего слайдера:
1
|
const sliderRenderer = css(slider);
|
И создайте value
для отслеживания смещения x нашего слайдера и обновите свойство translateX
слайдера, когда оно изменится:
1
|
const sliderX = value(0, (x) => sliderRenderer.set(‘x’, x));
|
Теперь переместить ползунок по горизонтали так же просто, как написать:
1
|
sliderX.set(-100);
|
Попытайся!
Сенсорная прокрутка
Мы хотим, чтобы наша карусель начала прокручиваться, когда пользователь перетаскивает ползунок горизонтально и останавливает прокрутку, когда пользователь перестает касаться экрана. Наши обработчики событий будут выглядеть так:
01
02
03
04
05
06
07
08
09
10
11
|
let action;
function stopTouchScroll() {
document.removeEventListener(‘touchend’, stopTouchScroll);
}
function startTouchScroll(e) {
document.addEventListener(‘touchend’, stopTouchScroll);
}
slider.addEventListener(‘touchstart’, startTouchScroll, { passive: false });
|
В нашей функции startTouchScroll
мы хотим:
- Остановите любые другие действия,
sliderX
. - Найдите исходную точку касания.
- Прослушайте следующее событие
touchmove
чтобы увидеть, перетаскивает ли пользователь вертикально или горизонтально.
После document.addEventListener
добавьте:
1
|
if (action) action.stop();
|
Это остановит любые другие действия (такие как физическая импульсная прокрутка, которую мы реализуем в stopTouchScroll
) от перемещения ползунка. Это позволит пользователю немедленно «поймать» ползунок, если он прокручивает мимо элемента или заголовка, на котором он хочет щелкнуть.
Далее нам нужно сохранить исходную точку касания. Это позволит нам увидеть, куда пользователь перемещает свой палец дальше. Если это вертикальное движение, мы разрешим прокрутку страницы как обычно. Если это горизонтальное движение, вместо этого мы прокручиваем ползунок.
Мы хотим поделиться этим touchOrigin
между обработчиками событий. Так что после let action;
добавлять:
1
|
let touchOrigin = {};
|
Вернувшись в наш обработчик startTouchScroll
, добавьте:
1
2
3
4
5
|
const touch = e.touches[0];
touchOrigin = {
x: touch.pageX,
y: touch.pageY
};
|
Теперь мы можем добавить слушатель события touchmove
к document
чтобы определить направление перетаскивания на основе этого touchOrigin
:
1
|
document.addEventListener(‘touchmove’, determineDragDirection);
|
Наша функция defineDragDirection собирается измерить местоположение следующего касания, проверить его фактическое перемещение и, если да, измерить угол, чтобы определить, является ли он вертикальным или горизонтальным:
1
2
3
4
5
6
7
|
function determineDragDirection(e) {
const touch = e.changedTouches[0];
const touchLocation = {
x: touch.pageX,
y: touch.pageY
};
}
|
Popmotion включает несколько полезных калькуляторов для измерения таких вещей, как расстояние между двумя координатами x / y. Мы можем импортировать такие как это:
1
|
const { calc, css, value } = window.popmotion;
|
Затем для измерения расстояния между двумя точками используется калькулятор distance
:
1
|
const distance = calc.distance(touchOrigin, touchLocation);
|
Теперь, если касание переместилось, мы можем отключить этот прослушиватель событий.
1
2
|
if (!distance) return;
document.removeEventListener(‘touchmove’, determineDragDirection);
|
Измерьте угол между двумя точками с помощью калькулятора angle
:
1
|
const angle = calc.angle(touchOrigin, touchLocation);
|
Мы можем использовать это, чтобы определить, является ли этот угол горизонтальным или вертикальным, передавая его следующей функции. Добавьте эту функцию в самый верх нашего файла:
01
02
03
04
05
06
07
08
09
10
11
12
|
function angleIsVertical(angle) {
const isUp = (
angle <= -90 + 45 &&
angle >= -90 — 45
);
const isDown = (
angle <= 90 + 45 &&
angle >= 90 — 45
);
return (isUp || isDown);
}
|
Эта функция возвращает true
если заданный угол находится в пределах -90 +/- 45 градусов (прямо вверх) или 90 +/- 45 градусов (прямо вниз). Таким образом, мы можем добавить еще одно условие return
, если эта функция возвращает true
.
1
|
if (angleIsVertical(angle)) return;
|
Отслеживание указателя
Теперь мы знаем, что пользователь пытается прокрутить карусель, мы можем начать отслеживать его пальцем. Popmotion предлагает действие указателя, которое выведет координаты x / y мыши или сенсорного указателя.
Сначала импортируйте pointer
:
1
|
const { calc, css, pointer, value } = window.popmotion;
|
Чтобы отслеживать сенсорный ввод, укажите исходное событие для pointer
:
1
|
action = pointer(e).start();
|
Мы хотим измерить начальную позицию x
нашего указателя и применить любое движение к ползунку. Для этого мы можем использовать преобразователь с именем applyOffset
.
Трансформаторы — это чистые функции, которые принимают значение и возвращают его — да — преобразованное. Например: const double = (v) => v * 2
.
1
2
|
const { calc, css, pointer, transform, value } = window.popmotion;
const { applyOffset } = transform;
|
applyOffset
это карри функция. Это означает, что когда мы вызываем его, он создает новую функцию, которой затем может быть передано значение. Сначала мы вызываем его с помощью номера, из которого мы хотим измерить смещение, в данном случае — текущего значения action.x
, и числа, к которому применяется это смещение. В данном случае это наш sliderX
.
Так что наша функция applyOffset
будет выглядеть так:
1
|
const applyPointerMovement = applyOffset(action.x.get(), sliderX.get());
|
Теперь мы можем использовать эту функцию в обратном вызове output
указателя, чтобы применить движение указателя к ползунку.
1
|
action.output(({ x }) => slider.set(applyPointerMovement(x)));
|
Остановка со стилем
Карусель теперь перетаскивается на ощупь! Вы можете проверить это с помощью эмуляции устройства в Chrome Developer Tools.
Чувствую себя немного дерганным, верно? Возможно, вы уже сталкивались с прокруткой, которая выглядит так: раньше вы поднимаете палец, и прокрутка останавливается. Или прокрутка останавливается, а затем появляется небольшая анимация, чтобы имитировать продолжение прокрутки.
Мы не собираемся этого делать. Мы можем использовать физическое действие в Popmotion, чтобы получить истинную скорость sliderX
и применить к ней трение в течение некоторого времени.
Во-первых, добавьте его в наш постоянно растущий список импорта:
1
|
const { calc, css, physics, pointer, value } = window.popmotion;
|
Затем в конце нашей функции stopTouchScroll
добавьте:
1
2
3
4
5
6
7
8
9
|
if (action) action.stop();
action = physics({
from: sliderX.get(),
velocity: sliderX.getVelocity(),
friction: 0.2
})
.output((v) => sliderX.set(v))
.start();
|
Здесь, from
и velocity
устанавливаются с текущим значением и скоростью sliderX
. Это гарантирует, что наше физическое моделирование имеет те же начальные начальные условия, что и перетаскивание пользователя.
friction
устанавливается как 0.2
. Трение задается как значение от 0
до 1
, где 0
— отсутствие трения вообще, а 1
— абсолютное трение. Попробуйте поиграть с этим значением, чтобы увидеть, какое изменение он вносит в «ощущение» карусели, когда пользователь перестает перетаскивать.
Меньшие числа заставят это чувствовать себя легче, и большие числа сделают движение более тяжелым. Для движения с прокруткой, я чувствую, что 0.2
ударил хороший баланс между неустойчивым и вялым.
границы
Но есть проблема! Если вы играли с новой сенсорной каруселью, это очевидно. У нас нет ограниченного движения, что позволяет буквально выбросить вашу карусель!
Для этой работы есть другой трансформатор, clamp
. Это также карри функция, то есть если мы вызовем ее с минимальным и максимальным значением, скажем, 0
и 1
, она вернет новую функцию. В этом примере новая функция будет ограничивать любое число, данное ей, от 0
до 1
:
1
|
clamp(0, 1)(5);
|
Во-первых, импортный clamp
:
1
|
const { applyOffset, clamp } = transform;
|
Мы хотим использовать эту функцию зажима для нашей карусели, поэтому добавьте эту строку после того, как мы определим minXOffset
:
1
|
const clampXOffset = clamp(minXOffset, maxXOffset);
|
Мы собираемся изменить два output
мы установили для наших действий, используя некоторую легкую функциональную композицию с преобразователем pipe
.
труба
Когда мы вызываем функцию, мы пишем это так:
1
|
foo(0);
|
Если мы хотим передать вывод этой функции другой функции, мы могли бы написать это так:
1
|
bar(foo(0));
|
Это становится немного трудным для чтения, и это только ухудшается, поскольку мы добавляем все больше и больше функций.
С помощью pipe
мы можем составить новую функцию из foo
и bar
которую мы можем использовать повторно:
1
2
|
const foobar = pipe(foo, bar);
foobar(0);
|
Это также написано в естественном порядке начала -> конца, который облегчает следовать. Мы можем использовать это, чтобы составить applyOffset
и applyOffset
в одной функции. Импортная pipe
:
1
|
const { applyOffset, clamp, pipe } = transform;
|
Замените output
обратный вызов нашего pointer
на:
1
2
3
4
5
6
|
pipe(
({ x }) => x,
applyOffset(action.x.get(), sliderX.get()),
clampXOffset,
(v) => sliderX.set(v)
)
|
И замените обратный вызов physics
на:
1
|
pipe(clampXOffset, (v) => sliderX.set(v))
|
Этот вид функциональной композиции может довольно аккуратно создавать описательные, пошаговые процессы из меньших, многократно используемых функций.
Теперь, когда вы перетаскиваете карусель, она не выходит за пределы своих границ.
Резкая остановка не очень удовлетворяет. Но это проблема для более поздней части!
Вывод
Это все для первой части. До сих пор мы рассматривали существующие карусели, чтобы увидеть сильные и слабые стороны различных подходов к прокрутке. Мы использовали отслеживание ввода и физику Popmotion для эффективной анимации translateX
нашей карусели с сенсорной прокруткой. Мы также познакомились с функциональной композицией и функциями карри.
Вы можете получить закомментированную версию «истории на данный момент» в этом CodePen .
В следующих выпусках мы рассмотрим:
- прокрутка колесиком мыши
- переосмысление карусели при изменении размеров окна
- нумерация страниц с клавиатурой и мышью
- восхитительные прикосновения, с помощью весенней физики
Ждем Вас там!