Доброго времени суток, уважаемые читатели и писатели!
Сегодня я расскажу, как в проекте передо мной возникла задача по изготовлению адаптивного слайдера и что из этого получилось
О статье и для кого она
Данную статью я пишу не столько потому, что желаю получить отклик сообщества на решение данной проблемы, но и потому, что решение вопросов статьи кажется мне фундаментальным для понимания адаптивности слайдера в вебе. Если кто то уже писал подобные компоненты просьба откликнуться и поделиться схожим опытом
Немножко о том, что случилось и какие инструменты были использованы
В React приложении необходимо сделать карусель (здесь и далее буду использовать это название), элементы которой гармонично смотрятся на экране любого размера. Есть несколько ограничений:
- Максимальная ширина элемента составляет 150рх
- Используемый инструмент — React Owl Carousel
- Максимальный размер контейнера для карусели — 1190рх
- Также есть показатели свойства padding для разных экранов (влияет на ширину видимой части контейнера) и margin (между элементами не менее 5рх)
- Карусель должна зацикливаться
И прочие условия, не имеющие влияния на предмет статьи
Отступление о механике работы карусели
Многие карусели (не исключением из них является React Owl Carousel) используют для показа специальный класс active, описывающий элементы, которые в данный момент демонстрируются на экране.
Для вывода на экран бесконечного цикла первые и последние элементы дублируются (механика и проблемы этого дубляжа есть тема для отдельной статьи).
Свойства описываются специальными объектами, интересовать нас будет объект responsive, который отвечает за переназначение свойств.
Остальные данные о механике работы будут понятны по ходу описания решения.
Первые возникшие проблемы
Сначала все шло гладко — были написаны и стилизованы сами элементы, прописаны основные свойства всей карусели. Проблемы начались при выставлении свойства {loop: true}
Карусель зацикливалась неадекватно
При прокручивании до конца списка в карусели оставалось свободное пространство и некоторое время прокручивалось именно оно.
Причина оказалась в максимальной ширине элемента, не согласованной с их количеством. Конкретным примером является ширина контейнера 1190рх, при этом количество элементов выставлено 3.
Другими словами, карусель ожидает, что 3 элемента растянутся на 1190рх, а они больше 150рх стать не могут.
Повышая количество элементов
Проблема приобретает другой ракурс: при слишком большом количестве элементов на контейнер ширина их становится слишком малой (а внутри них есть контент!) Если я задавал свойство min-width, то на некоторых размерах экрана элементы заползают друг на друга, игнорируя margin, что нарушает условия.
Резюмируем условия адаптивности
- Количество элементов на экране должно быть меньше отношения размера экрана к
минимальной ширине элемента — иначе даже элементы минимальной ширины не поместятся на экране. - Отношение размера экрана к предполагаемому количеству элементов не должно быть больше максимальной предполагаемой длины, иначе возникает проблема с зацикливанием.
- Описываемые выше условия должны быть соблюдены для любого размера экрана (от 330рх до 1190рх).
Решаем проблему как программисты
Если подходить к проблеме последовательно, очевидно, что чем-то придется поступиться, в моем случае это была минимальная ширина элемента.
Какова должна быть минимальная ширина элемента, чтобы для всех экранов контейнера условия адаптивности были выполнены?
// Названия переменных говорят сами за себя
const getBackTrace = (minScreen = 300, maxElementWidth = 150) => {
let backTrace = {}
for (let minElementWidth = maxElementWidth; minElementWidth > 0; minElementWidth--){
// Постепенно уменьшаем минимальную ширину до выполнения всех условий и фиксируем результат
// записывая его в объект backTrace
for(let screen = minScreen; screen <= 1100; screen++){
let elementCount = screen / minElementWidth | 0
if((screen / elementCount) > maxElementWidth){
backTrace[minElementWidth] = screen
break
}
}
}
for(let key in backTrace){
// Для удобства обходим объект и находим минимальную ширину, до которой приходится сжимать элементы
if (backTrace[key - 1] == undefined){
backTrace.result = key - 1
return backTrace
}
}
}
// getBackTrace(300, 150).result = 100
Результат в 100рх меня не устроил, так как не позволяет уместить весь контент в элементе. Следовательно, продолжаем поиски до нахождения нужного значения и ищем, чем еще можно жертвовать.
Помните подзаголовок? Для поиска напишем функцию
const getMinScreen = (minWidth = 300, maxWidth = 767, maxElementWidth = 150) => {
let research = []
// по сути, пробуем поменять минимальный размер контейнера и прогнать его через
// getBackTrace, пробуем уменьшить адаптивность в угоду контенту
for(let min = minWidth; min < maxWidth; min++){
let { result } = getBackTrace(min, maxElementWidth)
research.push({result, min})
}
// Перед возвращением уничтожим повторяющиеся значения и вернем корректный и "удобный к употреблению" объект
return research
.reduce((acc, curr, idx, arr) => {
let obj = {}
let {min, result} = curr
obj[min] = result
if(idx == 0) return obj
if(arr[idx-1].result == result){
return {...acc}
} else {
return {...acc, ...obj}
}
}, {})
}
/* Returned object
{300: 100,
303: 101,
306: 102,
309: 103,
312: 104,
315: 105,
318: 106,
321: 107,
324: 108,
327: 109,
330: 110,
333: 111,
336: 112,
452: 113,
456: 114,
460: 115,
464: 116,
468: 117,
472: 118,
476: 119,
480: 120}
Значения свыше 480 я не рассматривал
*/
Рассматривая полученный объект, видно большой скачок при переходе от 336рх к 452рх.
Я принял волевое решение ограничить адаптивность на 36рх.
Описываем адаптивный объект
Казалось бы, проблема решена, но такое решение только доказывает, что соблюдение условий возможно для экранов от 336рх, но не описывает способ. А ведь есть и разные условия, ограничивающие меня при производстве объекта со свойствами адаптивности
Приняв для себя, что минимальная ширина элемента без потерь может быть 107рх, варьируя значением margin, я пришел к следующим показателям:
Экран | margin | минимальная ширина |
---|---|---|
336+ | 5 | 107 |
468+ | 10 | 107 |
763+ | 15 | 112 |
Осталось дело за малым — собрать полученные данные в кучу и реализовать адаптивный объект:
getResponsiveOwlItems = () => {
let responsive = {};
responsive[0] = {items: 2, nav: false}
// 112 = 107 (minimal div) + 5 (margins)
let itemMinWidthReference = 112;
const getOneWidth = deviceWidth => deviceWidth / itemMinWidthReference | 0
// 1190 - container width
for(let i = itemMinWidthReference * 3 + 20; i <= 1190; i += itemMinWidthReference){
// .container padding > 768 90px + padding 90(.container)
// .container padding < 768 40px + padding -40(.container)
// +20px stagePadding
let padding = i > 767 ? 200 : 20
if(i > (468 + padding)) {
itemMinWidthReference = 117
}
if(i > (767 + padding)) {
itemMinWidthReference = 127
}
let items = getOneWidth(i - padding)
let nav = i > 700 ? true : false
let margin = 5;
if (i > 468){
margin = 10
}
if (i > 767){
margin = 15
}
responsive[i.toString()] = {items, nav, margin}
// для выравнивания брейкпоинтов при изменениях itemMinWidthReference
i = i - (i % itemMinWidthReference) + 1
}
return responsive;
}
На день публикации все выглядит логично, и я не смог воспроизвести ошибку в карусели — вероятно, все работает как предполагалось.
Спасибо за внимание, жду Ваших комментариев и замечаний!
Комментарии (8)
apapacy
27.07.2019 22:35Я конечно не вникал в работу именно этого компонента.
Но у меня в первую очередь вознико бы наверное желание поискать и заюзать более распространенный компонент например react-slick.neostack.com По двум причинам. Первая, что более распространенный компонент за счет активной обратной связи работает более предсказуемо. Вторая, просто по себе знаю как бывает неудобно разбирать чужой код особенно если он борется с такими вот проблемами.Konstantin_Loginovskikh Автор
27.07.2019 14:48Спасибо за развернутый ответ и ссылку на компонент — обязательно изучу его!
В данном случае я привязан к чужой кодовой базе и мне не разрешено изменять инструменты, уже работающие на проекте
Поэтому это даже не OwlCarousel2, а OwlCarousel
androidovshchik
27.07.2019 08:23У вас очень много магических чисел в коде, не есть хорошо
Konstantin_Loginovskikh Автор
27.07.2019 14:52Согласен
Но! Я минимально оставил магические числа в исследовательских функциях (1я и 2я) и в рабочей функции у меня уже дрогнула рука. В проекте их я уже убрал, спасибо Вам за дельное замечание!
danilovmy
27.07.2019 21:30Раз так много заложено на стилях, может попробовать вариант исключительно через CSS с анимацией и потом доработать скриптом для управления? у нас так реализованы бэкграунд слайдеры.
codemafia
28.07.2019 00:35Стиль написания кода считаю неприемлемым для материалов на Хабре.
С ужасом представляю, как один из падаванов натыкается на эту статью и, полагаясь на репутацию ресурса, начинает копипастить это в проекты, а то и вовсе писать в таком же стиле.
ArsenAbakarov
Почему не устроили существующие решения?
Konstantin_Loginovskikh Автор
К сожалению, мне не удалось найти существующие решения, которые отвечали бы моим требованиям (описаны в начале статьи)