Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. JavaScript
Code

Создание безупречной карусели. Часть 3

by
Difficulty:AdvancedLength:LongLanguages:
This post is part of a series called Create the Perfect Carousel.
Create the Perfect Carousel, Part 2

Russian (Pусский) translation by AlexBioJS (you can also view the original English article)

Это третья и последняя часть серии руководств "Создание безупречной карусели". В первой части мы оценили карусели на сайтах Netflix и Amazon, две из наиболее активно используемых каруселей в мире. Мы создали карусель и реализовали возможность ее прокрутки при помощи касаний пальцами.

Затем во второй части мы добавили возможность прокрутки контента по горизонтали, пагинации и индикатор хода процесса. Оп, и готово.

Теперь, в последней части серии, мы заглянем в тяжелый для понимания и часто упускаемый мир возможности управления каруселью при помощи клавиатуры. Мы внесем поправки в код для выполнения перерасчета размера карусели при изменении размера окна просмотра (* видимая для пользователя область веб-страницы. Здесь и далее примеч. пер.). И, наконец, мы добавим несколько последних штрихов при помощи эффекта усилия пружины.

Вы можете освежить в памяти, где мы остановились, посетив этот Pen (* фрагмента кода) на Codepen.

Возможность управления каруселью при помощи клавиатуры

Действительно, большинство пользователей не используют клавиатуру для навигации, поэтому, к сожалению, мы иногда забываем о тех, кто использует. В некоторых странах недоступность веб-сайта для некоторых категорий населения может рассматриваться как противоправное действие. Еще хуже то, что это подлый поступок.

Хорошая новость заключается в том, что эту возможность обычно легко реализовать! На самом деле браузера делают большую часть работы за нас. Серьезно: попробуйте попереходить по элементам созданной нами карусели при помощи клавиши табуляции. Поскольку мы воспользовались смысловой HTML-разметкой (* когда сами элементы до некоторой степени подсказывают, что за контент в них находится), то вы уже можете!

Однако вы заметите, что наши навигационные кнопки пропадают. Это так, поскольку браузер не позволяет сфокусироваться на элементе, который располагается за пределами нашего окна просмотра. Поэтому, несмотря на то что мы задали правило overflow: hidden, мы не можем прокрутить карусель по горизонтали; в ином случае она и правду будет прокручиваться для показа элемента в фокусе.

Это нормально, и такое решение считалось бы, на мой взгляд, «работоспособным», хотя и не совсем восхитительным.

Карусель на Netflix работает таким же образом. Однако поскольку большинство их заголовков подгружаются по мере необходимости (* по мере прокручивания пользователем страницы), а также имеется пассивная возможность их просмотра при помощи клавиатуры (то есть они не писали специально никакого кода для реализации этой возможности), мы не можем собственно получить никаких заголовков за исключением тех, что уже загрузились. Это также выглядит ужасно:

Keyboard Accessibility

Мы можем сделать лучше.

Обработка события focus

Для этого мы будем прослушивать событие focus, возникающее при фокусировании любого элемента карусели. При попадании элемента в фокус мы запросим у него его позицию. Затем мы сверим это значение со sliderX и sliderVisibleWidth, чтобы узнать, находится ли тот элемент в пределах окна просмотра. Если нет, то мы перейдем к нему при помощи того же кода, что написали в части 2.

В конце функции carousel добавьте следующий слушатель событий:

Вы заметите, что мы передали третий аргумент, true. Вместо назначения обработчика событий каждому элементу мы можем воспользоваться приемом разработки под названием делегирование событий, чтобы прослушивать события, возникающие только для одного элемента, их прямого предка. Событие focus не всплывает, поэтому при помощи true обработчику дается указание прослушивать событие на стадии перехвата, стадии, когда событие возникает для каждого элемента, начиная с window до целевого элемента (в нашем случае это элемент, попавший в фокус).

Выше нашего растущего набора обработчиков добавьте функцию onFocus:

Мы будем работать в этой функции до конца текущего раздела.

Нам необходимо измерить отступы левой и правой сторон элемента и проверить, лежит ли какое-либо из этих значений за пределами текущей области просмотра.

Мы получаем целевой элемент при помощи свойства target события и можем измерить его за счет getBoundingClientRect:

Значения left и right заданы относительно окна просмотра, а не карусели. Так что нам необходимо получить отступ левой стороны контейнера карусели, чтобы учесть это. В нашем примере это будет 0, но для того чтобы карусель была надежной, необходимо, чтобы учитывалось то, что она может быть помещена где угодно.

Далее мы можем просто проверить, располагается ли элемент за пределами видимой области карусели, и перейти в том направлении:

Теперь, при нажатии клавиши табуляции, в карусели четко происходит переход по элементам при помощи нашей клавиатурной фокусировки (* состояние окна или элемента изображения, при котором они принимают данные с клавиатуры)! Всего лишь несколько строк кода для проявления дополнительной заботы о пользователях.

Перерасчет размера карусели

Вы, вероятно, заметили по ходу работы с руководством, что при изменении размера окна просмотра вашего браузера переход по элементам карусели уже не работает должным образом. Это так, поскольку мы измерили ширину карусели относительно ее видимой области лишь единожды, в момент инициализации (* установка известного исходного состояния, например присваивание начальных значений переменным).

Для того чтобы убедиться в том, что наша карусель работает как следует, нам необходимо заменить некоторую часть нашего кода для вычисления размеров на новый обработчик событий, который срабатывает при изменении размеров окна.

Теперь, недалеко от начала нашей функции carousel, сразу после строки, где мы определяем progressBar, нам нужно заменить три из этих результатов вычислений размеров, хранящихся в константах, объявленных при помощи const, на значения, что хранятся в переменных, объявленных при помощи let, поскольку они будут меняться при изменениях окна просмотра:

Затем мы можем переместить логику, при помощи которой ранее вычислялись эти значения размеров, на новую функцию под названием measureCarousel:

Нам необходимо сразу же вызвать эту функцию так, чтобы эти значения по-прежнему задавались при инициализации: Прямо в следующей строке вызовите measureCarousel:

Карусель должна работать так же, как и ранее. Для обновления результатов вычисления размеров при изменении размера окна мы просто добавляем следующий обработчик событий в самом конце функции carousel:

Теперь, если вы измените размер карусели и попробуете попереходить по элементам, то карусель продолжит работать как положено.

Примечание по поводу производительности

Стоит учитывать, что в реальном мире разработки у вас может быть несколько каруселей на одной и той же странице, из-за чего влияние кода для перерасчета размера карусели на производительность приумножается на их количество.

Как мы коротко обсудили в части 2, неразумно выполнять ресурсоемкие вычисления чаще, чем это необходимо. Мы сказали, что в случае событий, возникающих при прокрутке и действиях с мышкой, вам нужно, чтобы они выполнялись единожды за фрейм для поддержания частоты смены кадров (* скорость сканирования или вывода на экран видеокадров - дискретных изображений (30 кадр/с в стандарте NTSC и 25 кадр/с в стандарте PAL/SECAM)) на уровне 60 кадров в секунду. События, возникающие при изменении размеров окна, немного отличаются тем, что при их возникновении размеры элементов всего документа будут повторно рассчитаны и некоторые элементы перераспределятся, что представляет из себя, вероятно, наиболее ресурсоемкий момент, который может случиться с веб-страницей.

Нам не нужно повторно рассчитывать размер карусели до тех пор, пока пользователи не перестали менять размер окна, поскольку они не будут взаимодействовать с ним в это время. Мы можем обернуть нашу функцию measureCarousel в специальную функцию под названием debounce (* от англ. устранение дребезга).

При помощи функции debounce по сути сообщается следующее: «Запускать эту функцию только в том случае, если она не вызывалась в течение x миллисекунд». Вы можете обратиться за дополнительной информацией о debounce к замечательному учебнику для начинающих, написанному David Walsh, а также ознакомиться там с некоторыми примерами кода.

Последние штрихи

На данный момент у нас получилась довольно хорошая карусель. Она доступна для применения (* атрибут (свойство) программного или аппаратного продукта, характеризующий возможность использовать этот продукт для людей с физическими ограничениями), красиво анимируется, работает на устройствах, управляемых при помощи касаний, и на устройствах, управляемых при помощи мышки, и обладает такой большой гибкостью, которую карусели с изначально имеющейся возможностью прокрутки, не предоставляют.

Однако это не серия руководств под названием "Создание довольно хорошей карусели". Пришло время немного покрасоваться, и для этого у нас имеется секретное оружие. Эффекты усилия пружин.

Мы добавим два таких эффекта. Один для случаев, когда каруселью управляют при помощи касаний пальцами, а другой для случаев, когда каруселью управляют при помощи пагинации. Они оба позволят пользователям понять в веселой и увлекательной манере, что они достигли конца карусели.

Эффект усилия пружины при касаниях пальцами

Для начала давайте добавим дерганье в стиле iOS для случаев, когда пользователь пытается прокрутить карусель за ее пределы. В данный момент мы задаем пределы прокрутки при помощи касаний за счет clampXOffset. Давайте заменим ее некоторым кодом, за счет которого добавляется дерганье при выходе значения рассчитанного отступа за пределы границ карусели.

Для начала нам необходимо импортировать преобразователь для создания нашей пружины. Имеется преобразователь под названием nonlinearSpring, за счет которого к позиции, заданной при помощи предоставленного ему нами числа, применяется экспоненциально возрастающая сила по направлению к origin (* от англ. начало). Это означает, что чем дальше мы тянем карусель, тем быстрее она отскочит обратно.  Мы можем импортировать его следующим образом:

В функции determineDragDirection у нас имеется следующий код:

Сразу выше него давайте создадим наши две пружины, по одной для каждого предела прокрутки карусели:

Значение elasticity (* от англ. эластичности) определяется путем экспериментирования и выбора того, что вам подходит. Задайте  его слишком низким, и пружина будет казаться слишком тугой. Задайте его слишком высоким, и вы не заметите ее отскока, или еще хуже, она оттянет карусель еще дальше от пальца пользователя!

Теперь нам всего лишь необходимо написать простую функцию, при помощи которой будет применена одна из этих двух пружин, если переданное значение находится за пределами допустимого диапазона:

Мы можем заменить clampXOffset в коде выше на applySpring. Теперь, если вы оттянете карусель за ее пределы, то она отскочит обратно!

Но когда мы отпускаем пружину, то она как бы просто отскакивает в исходную позицию. Нам нужно внести изменения в функцию stopTouchScroll, при помощи которой реализуется возможность инерциальной прокрутки, для проверки того, находится ли карусель по-прежнему за пределами допустимого диапазона, и, если это так, применить вместо ранее добавленной пружину, реализуемую при помощи действия physics (* каждая анимация Popmotion является действием).

Эффект усилия пружины, реализуемый при помощи physics

При помощи действия physics также можно симулировать пружины. Нам лишь необходимо передать этой функции в объекте свойства spring и to.

В stopTouchScroll переместите имеющийся код для инициализации функции physics, за счет которой реализуется возможность прокрутки контента при помощи касаний, в часть логики, при помощи которой гарантируется, что мы находимся в пределах прокрутки:

Мы знаем, что в первой ветви инструкции if выполняется код для случаев, когда карусель находится за пределами границ прокрутки, так что мы можем добавить нашу пружину:

Мы хотим создать эффект усилия тугой и отзывчивой пружины. Я выбрал относительно высокое значение spring для получения резкого «щелчка» и понизил значение friction до 0.92, чтобы добавить небольшой отскок. Вы могли бы задать в качестве значения трения 1, чтобы совсем убрать отскок.

В качестве небольшого домашнего задания попробуйте заменить clampXOffset в методе output объекта physics, за счет которого реализуется возможность прокрутки контента при помощи касаний, на функцию, при помощи которой добавляется подобный эффект пружины, когда отступ по оси Х достигает границ карусели. Вместо имеющейся на данный момент резкой остановки попробуйте сделать так, чтобы она смягчалась в конце.

Эффект усилия пружины при пагинации

Владельцам устройств, управляемых при помощи касаний, всегда предоставляется крутой эффект усилия пружины, верно? Давайте позаботимся и о пользователях настольных компьютеров, определяя момент, когда карусель находится у границ своей прокрутки, и добавляя подсказывающее дерганье, чтобы четко и ясно показать пользователям, что они находятся в конце.

Во-первых, нам необходимо сделать так, чтобы кнопки для пагинации становились неактивными по достижении границ прокрутки. Для начала давайте добавим правило CSS, при помощи которого для кнопок задается специальное стилевое оформление, чтобы показать, что они неактивны. В правиле для button добавьте:

Мы используем здесь класс вместо более смыслового атрибута disabled, поскольку нам по-прежнему нужно, чтобы срабатывали обработчики события click, чему, как следует из имени, disabled помешало бы.

Добавьте этот класс disabled кнопке Prev, поскольку изначально отступ каждой карусели имеет значение 0:

Ближе к верхней части функции carousel создайте новую функцию под названием checkNavButtonStatus. Нам нужно, чтобы при помощи этой функция переданное значение просто сравнивалось с minXOffset и maxXOffset и соответствующим образом добавлялся класс disabled кнопки:

Заманчиво было бы вызывать ее каждый раз при изменении значения sliderX. В этом случае кнопки начали бы мигать каждый раз при колебании пружины около границ прокрутки. Также это бы привело к странному поведению карусели, если бы была нажата одна из кнопок во время одного из тех анимационных эффектов усилия пружины. Дерганье «конца прокрутки» должно всегда запускаться при достижении конца карусели, даже если выполняется анимационный эффект усилия пружины, из-за которого она оттягивается от абсолютного конца.

Так что нам необходимо быть более избирательными насчет того, где вызывать вышеупомянутую функцию. Кажется целесообразным вызывать ее в нижеперечисленных местах:

В последней строке onWheel добавьте checkNavButtonStatus(newX);.

В последней строке goto добавьте checkNavButtonStatus(targetX);.

И, наконец, в конце determineDragDirection и в ветви кода для добавления возможности инерциальной прокрутки (код, находящийся в блоке else) функции stopTouchScroll замените:

На:

Теперь все, что нам осталось, – добавить в функции gotoPrev и gotoNext код для проверки наличия класса disabled в classList кнопок, при помощи которых они запускаются, и выполнить пагинацию только в том случае, если он отсутствует:

При помощи функции notifyEnd создается просто еще одна пружина, реализуемая при помощи physics, и она выглядит следующим образом:

Поэкспериментируйте с ней и, опять-таки, подгоните значения аргументов physics, как вам нужно.

Остался только один небольшой баг. При оттягивании карусели за пределы крайней левой границы индикатор хода процесса поворачивается в обратном направлении. Мы можем это быстро исправить, заменив:

На:

Мы могли бы предотвратить продвижение индикатора и в другое направление, однако лично мне кажется крутым то, что он отображает движение пружины. Просто кажется странным, когда он переворачивается.

Прибираем за собой

В случае одностраничных приложений веб-контент остается дольше в сессии пользователя (* по сравнению с обычными). Часто бывает так, что при изменении страницы по-прежнему остается запущенной та же среда исполнения JS, что и при начальной загрузке. Мы не можем рассчитывать на наличие чистого состояния при каждом переходе пользователя по ссылке, и это означает, что мы должны прибрать за собой, чтобы предотвратить срабатывание обработчиков событий, зарегистрированных для мертвых элементов.

В React код для этого помещен в метод componentWillLeave. Во Vue с этой целью используется beforeDestroy. В этой серии руководств мы реализуем карусель только при помощи JS, однако мы по-прежнему можем предоставить метод для отвязывания обработчиков, которых работал бы сходным образом в любом фреймворке.

На данный момент функция carousel ничего не вернула. Давайте это изменим.

Для начала давайте изменим последнюю строку, в которой происходит вызов carousel, на:

Мы вернем из carousel только функцию, при помощи которой отвязываются все обработчики событий. В самом конце функции carousel добавьте следующий код:

Теперь, если вы вызовите destroyCarousel и попробуете поиграться с каруселью, то ничего не произойдет! Немного печально видеть ее в таком состоянии.

И на этом все!

Фух-х-х. Мы со многим разобрались! Что у нас получилось. Вы можете ознакомиться с конечным результатом, посетив этот Pen на Codepen. В этой последней части мы добавили возможность управления каруселью при помощи клавиатуры, возможность перерасчета размера карусели при изменении размера окна просмотра, несколько прикольных дополнений, реализованных при помощи эффекта усилия пружины, и огорчительный, но обязательный этап разрушения всего созданного.

Надеюсь, что вам понравилось работать с этим руководством так же, как и мне писать его. Я бы хотел услышать ваши мысли по поводу дальнейших способов улучшения доступности применения или добавления дополнительных прикольных небольших штрихов.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.