eFusion 11 мая 2020

📸 Как сделать галерею в стиле Instagram

Галереи картинок с горизонтальной прокруткой выглядят современно и узнаваемо. Рассказываем, как с помощью CSS и JS перенести знакомую пользователям механику в ваши веб-приложения.
0
7508

В этой статье мы реализуем два типа горизонтальных списков и галерею. Вначале в теоретической части опишем, какое поведение элементов интерфейса хотим получить. В практической части рассмотрим, как воплотить необходимую механику в CSS и JavaScript.

Теория: горизонтальные списки и галерея

Приведённая ниже анимация иллюстрирует горизонтальный список со свободным скроллингом. Структуру можно свободно прокручивать влево и вправо. Список можно слегка оттягивать на крайних элементах. Это даёт подсказку пользователю о границах объекта.

Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)
Горизонтальный список со свободной прокруткой (free-scrolling horizontal lists)

Горизонтальный список со снэппингом – почти то же самое, но текущий элемент списка при тапе заменяется на следующий.

Горизонтальный список со снэппингом (snapping horizontal lists)
Горизонтальный список со снэппингом (snapping horizontal lists)

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

Третий рассматриваемый элемент – галерея. Она похожа на snapping-списки, но каждый элемент занимает всё поле, как в обычном посте Instagram из нескольких фотографий. Под картинками расположен ряд точек – по одной для каждого изображения. Пользователю понятно, что есть другие изображения и какое из них видно в данный момент.

Пример галереи
Пример галереи

Практическая часть

1. Горизонтальные списки со свободной прокруткой

Для горизонтального списка используем flexbox, которому разрешим горизонтальный скроллинг. Размер элементов списка укажем явно, как и зазор между ними. Поля установим бо́льшими, чем зазор, чтобы видеть, когда мы докрутили до начала или конца списка.

        .list {
  display: flex;
  padding: 20px;
  overflow-x: scroll;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) { margin-right: 10px; }
    

Интерактивное представление и полный код доступны в в сэндбоксе:

Работает, но можно улучшить.

Поведение при прокрутке влево. В некоторых браузерах свайп влево повторяет действие кнопки «Назад». Такое поведение можно предотвратить, установив для overscroll-behavior значение contain.

Полосу прокрутки можно скрыть, установив overflow-x в auto. Однако при скроллинге полоса вновь появится. Чтобы скрыть полностью, можно установить scrollbar-width в none. На момент написания статьи это работало только в Firefox. Для других браузеров придется добавить трюкачества в CSS:

        .list { -ms-overflow-style: none; }
.list::-webkit-scrollbar { display: none; }
    

Плавная прокрутка. В iOS не хватает стандартной плавной прокрутки (momentum scrolling). Потребуется добавить -webkit-overflow-scrolling: touch;.

Предотвращение вертикальной прокрутки. Мы можем прокручивать страницу по вертикали. Эту возможность лучше отключить для пользователей мобильных устройств, добавив к списку touch-action: pan-x. Однако если список покрывает всю видимую область просмотра, а на странице есть что-то ещё, эти элементы станут недоступны. То есть такой вариант надо использовать с осторожностью.

В результате наших преобразований соответствующая часть CSS теперь будет иметь следующий вид:

        .list {
  display: flex;
  overflow-x: scroll;
  padding: 20px;

  /* Предотвращает случайное использование "Назад"*/
  overscroll-behavior: contain;

  /* Скрывает полосу прокрутки */
  scrollbar-width: none;
  -ms-overflow-style: none;

  /* Плавная прокрутка на iOS */
  -webkit-overflow-scrolling: touch;

  /* Отключает вертикальный скроллинг для тач-устройств */
  touch-action: pan-x;
}

/* Скрывает полосу прокрутки  */
.list::-webkit-scrollbar {
  display: none;
}

.item {
  height: 224px;
  width: 125px;
  flex-shrink: 0;
}

.item:not(:last-child) {
  margin-right: 10px;
}

/* фикс для отступов в конце списка */
.item:last-child {
  position: relative;
}

.item:last-child::after {
  position: absolute;
  left: 100%;
  height: 1px;
  width: 20px;
  display: block;
  content: "";
}

    

А результат выглядит вот так (там же полный код примера):

2. Горизонтальный список со снэппингом

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

        .list {
  scroll-snap-type: x mandatory;
}
    

Элементам списка добавляем опцию scroll-snap-align: start. Чтобы дать подсказку, следующий элемент может немного «выглядывать» из-за края экрана. Для этого достаточно установить отступ прокрутки: scroll-padding-inline-start: 20px. Если хочется свайпнуть одним движением несколько элементов, можно добавить scroll-snap-stop: always к элементам списка, но не все браузеры пока такое поддерживают.

Код полностью также выложен на Codesandbox:

3. Галерея в стиле Instagram

Если мы сделаем наш предыдущий список таким же широким, как область прокрутки, и удалим отступы, он будет выглядеть и вести себя примерно так же, как галерея Instagram. За исключением маленьких точек. Без точек будет выглядеть так:

Но как мы поясняли в теоретической части, эти маленькие точки – полезный графический приём. Получается, что нам нужно следить за тем, какой из элементов списка сейчас виден. Наиболее простой способ – использовать IntersectionObserver.

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

Вот как это будет выглядеть:

        // ссылки на элементы DOM
const list = document.querySelector('.list');
const items = Array.from(document.querySelectorAll('.item'));
const indicators = Array.from(document.querySelectorAll('.indicator'));

// создание наблюдателя
const observer = new IntersectionObserver(onIntersectionObserved, {
  root: list,
  threshold: 0.6
});

// наблюдаем за каждым элементом
items.forEach(item => {
  observer.observe(item);
});

// когда observer обнаруживает изменение записи
// (пункт, входящий в список)
// и эта запись пересекается,
// получаем индекс пересекающегося элемента
// устанавливаем нужный индикатор в активное положение
function onIntersectionObserved(entries) {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const intersectingIndex = items.indexOf(entry.target);
      activateIndicator(intersectingIndex);
    }
  });
}

function activateIndicator(index) {
  indicators.forEach((indicator, i) => {
    indicator.classList.toggle('active', i === index);
  });
}
    

Полный результат также доступен в сэндбоксе:

Примечание
Мы установили пороговое значение 0.6. То есть если 60% элемента находится в поле зрения, то мы считаем его «пересекающимся». Если установим значение равным 1, то пересекаемым будет считать только полностью видимый элемент.

Заключение

Мы разобрали несколько вариаций построения горизонтальных списков изображений. У приведённых решений есть и свои недостатки, и преимущества. Поскольку используется нативная прокрутка, нет возможности настроить способ движения, мы не можем контролировать «липкость» ползунка (в этом случае правильнее было бы использовать решение на JavaScript). Кроме того, в нашем решении мы использовали не самые стандартные подходы в CSS. Однако объём кода невелик, и он работает даже в старых браузерах.

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

Senior JS разработчик
по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG