Доброго времени суток, уважаемые читатели и писатели!


Сегодня я расскажу, как в проекте передо мной возникла задача по изготовлению адаптивного слайдера и что из этого получилось


О статье и для кого она


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


Немножко о том, что случилось и какие инструменты были использованы


В React приложении необходимо сделать карусель (здесь и далее буду использовать это название), элементы которой гармонично смотрятся на экране любого размера. Есть несколько ограничений:


  1. Максимальная ширина элемента составляет 150рх
  2. Используемый инструмент — React Owl Carousel
  3. Максимальный размер контейнера для карусели — 1190рх
  4. Также есть показатели свойства padding для разных экранов (влияет на ширину видимой части контейнера) и margin (между элементами не менее 5рх)
  5. Карусель должна зацикливаться
    И прочие условия, не имеющие влияния на предмет статьи

Отступление о механике работы карусели


Многие карусели (не исключением из них является React Owl Carousel) используют для показа специальный класс active, описывающий элементы, которые в данный момент демонстрируются на экране.


Для вывода на экран бесконечного цикла первые и последние элементы дублируются (механика и проблемы этого дубляжа есть тема для отдельной статьи).


Свойства описываются специальными объектами, интересовать нас будет объект responsive, который отвечает за переназначение свойств.


Остальные данные о механике работы будут понятны по ходу описания решения.


Первые возникшие проблемы


Сначала все шло гладко — были написаны и стилизованы сами элементы, прописаны основные свойства всей карусели. Проблемы начались при выставлении свойства {loop: true}


Карусель зацикливалась неадекватно


При прокручивании до конца списка в карусели оставалось свободное пространство и некоторое время прокручивалось именно оно.


Причина оказалась в максимальной ширине элемента, не согласованной с их количеством. Конкретным примером является ширина контейнера 1190рх, при этом количество элементов выставлено 3.


Другими словами, карусель ожидает, что 3 элемента растянутся на 1190рх, а они больше 150рх стать не могут.


Повышая количество элементов


Проблема приобретает другой ракурс: при слишком большом количестве элементов на контейнер ширина их становится слишком малой (а внутри них есть контент!) Если я задавал свойство min-width, то на некоторых размерах экрана элементы заползают друг на друга, игнорируя margin, что нарушает условия.


Резюмируем условия адаптивности


  1. Количество элементов на экране должно быть меньше отношения размера экрана к
    минимальной ширине элемента — иначе даже элементы минимальной ширины не поместятся на экране.
  2. Отношение размера экрана к предполагаемому количеству элементов не должно быть больше максимальной предполагаемой длины, иначе возникает проблема с зацикливанием.
  3. Описываемые выше условия должны быть соблюдены для любого размера экрана (от 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)


  1. ArsenAbakarov
    27.07.2019 21:56

    Почему не устроили существующие решения?


    1. Konstantin_Loginovskikh Автор
      27.07.2019 14:46

      К сожалению, мне не удалось найти существующие решения, которые отвечали бы моим требованиям (описаны в начале статьи)


  1. apapacy
    27.07.2019 22:35

    Я конечно не вникал в работу именно этого компонента.
    Но у меня в первую очередь вознико бы наверное желание поискать и заюзать более распространенный компонент например react-slick.neostack.com По двум причинам. Первая, что более распространенный компонент за счет активной обратной связи работает более предсказуемо. Вторая, просто по себе знаю как бывает неудобно разбирать чужой код особенно если он борется с такими вот проблемами.


    1. Konstantin_Loginovskikh Автор
      27.07.2019 14:48

      Спасибо за развернутый ответ и ссылку на компонент — обязательно изучу его!
      В данном случае я привязан к чужой кодовой базе и мне не разрешено изменять инструменты, уже работающие на проекте
      Поэтому это даже не OwlCarousel2, а OwlCarousel


  1. androidovshchik
    27.07.2019 08:23

    У вас очень много магических чисел в коде, не есть хорошо


    1. Konstantin_Loginovskikh Автор
      27.07.2019 14:52

      Согласен
      Но! Я минимально оставил магические числа в исследовательских функциях (1я и 2я) и в рабочей функции у меня уже дрогнула рука. В проекте их я уже убрал, спасибо Вам за дельное замечание!


  1. danilovmy
    27.07.2019 21:30

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


  1. codemafia
    28.07.2019 00:35

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