Как я делал действительно адаптивный слайдер (карусель)
- воскресенье, 28 июля 2019 г. в 00:19:46
Доброго времени суток, уважаемые читатели и писатели!
Сегодня я расскажу, как в проекте передо мной возникла задача по изготовлению адаптивного слайдера и что из этого получилось
Данную статью я пишу не столько потому, что желаю получить отклик сообщества на решение данной проблемы, но и потому, что решение вопросов статьи кажется мне фундаментальным для понимания адаптивности слайдера в вебе. Если кто то уже писал подобные компоненты просьба откликнуться и поделиться схожим опытом
В React приложении необходимо сделать карусель (здесь и далее буду использовать это название), элементы которой гармонично смотрятся на экране любого размера. Есть несколько ограничений:
Многие карусели (не исключением из них является React Owl Carousel) используют для показа специальный класс active, описывающий элементы, которые в данный момент демонстрируются на экране.
Для вывода на экран бесконечного цикла первые и последние элементы дублируются (механика и проблемы этого дубляжа есть тема для отдельной статьи).
Свойства описываются специальными объектами, интересовать нас будет объект responsive, который отвечает за переназначение свойств.
Остальные данные о механике работы будут понятны по ходу описания решения.
Сначала все шло гладко — были написаны и стилизованы сами элементы, прописаны основные свойства всей карусели. Проблемы начались при выставлении свойства {loop: true}
При прокручивании до конца списка в карусели оставалось свободное пространство и некоторое время прокручивалось именно оно.
Причина оказалась в максимальной ширине элемента, не согласованной с их количеством. Конкретным примером является ширина контейнера 1190рх, при этом количество элементов выставлено 3.
Другими словами, карусель ожидает, что 3 элемента растянутся на 1190рх, а они больше 150рх стать не могут.
Проблема приобретает другой ракурс: при слишком большом количестве элементов на контейнер ширина их становится слишком малой (а внутри них есть контент!) Если я задавал свойство min-width, то на некоторых размерах экрана элементы заползают друг на друга, игнорируя margin, что нарушает условия.
Если подходить к проблеме последовательно, очевидно, что чем-то придется поступиться, в моем случае это была минимальная ширина элемента.
Какова должна быть минимальная ширина элемента, чтобы для всех экранов контейнера условия адаптивности были выполнены?
// Названия переменных говорят сами за себя
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;
}
На день публикации все выглядит логично, и я не смог воспроизвести ошибку в карусели — вероятно, все работает как предполагалось.
Спасибо за внимание, жду Ваших комментариев и замечаний!