Как создавать плавные и отзывчивые карусели с помощью собственных компонентов

ScrollView: один компонент для управления ими всеми?

ScrollView - один из самых фундаментальных компонентов приложения React Native, который может действовать как отдельный компонент, а также как зависимость для ряда других более сложных компонентов.

Это впечатляет, если подумать о его разнообразных сценариях использования. Его можно использовать как автономное решение для вертикальной прокрутки, автоматически делая ваш контент подходящим для экрана любой высоты. Другие более сложные компоненты, такие как SectionList и FlatList, полагаются на ScrollView и его свойства, тогда как компоненты, управляемые сообществом, такие как KeyboardAwareScrollView, расширяют функциональность ScrollView для решения сложных проблем UX с виртуальной клавиатурой.

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

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

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

Как работают карусели со ScrollView

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

Прежде всего, компонент ScrollView должен быть настроен на горизонтальную структуру содержимого, а не на вертикальную. Установив для свойства horizontal значение true, ScrollView автоматически разместит свои дочерние компоненты в столбцах, а не в строках.

Это настраивает нас на то, чтобы попытаться использовать другие свойства для оптимизации поведения представления (подробнее о свойствах в следующем разделе).

‹Карусель /› Структура компонентов

Чтобы наглядно представить, как компонент ScrollView используется с другими компонентами для формирования карусели, рассмотрим следующую иллюстрацию:

Здесь есть трехкомпонентная базовая установка:

  • <ScrollView /> находится внутри компонента <Container />, который определяет видимую область карусели. Здесь можно стилизовать границы, цвета фона и т. Д., Придавая некоторый контекст прокручиваемой области.
  • Сам <ScrollView />. На этом этапе важно упомянуть, что для самого <ScrollView /> требуется предварительно определенный width в его contentContainerStyle опоре. Без предопределенной ширины мы не смогли бы прокручивать ScrollView - мы исследуем некоторый код ниже, чтобы вычислить ширину на основе того, сколько интервалов удерживает карусель, а также привязку к началу каждого интервала, когда прокрутка.

Высота не имеет большого значения - ее можно установить как фиксированное значение или установить автоматически в зависимости от содержимого карусели, в зависимости от вашего дизайна.

  • Каждый <Item />, представляющий один элемент карусели. Здесь будет размещаться ваш отображаемый контент, например ряд статистики, слайд с описанием приложения и т. Д. В зависимости от вашего дизайна один <Item /> может охватывать 100% ширины <Container /> или половину или даже треть видимая область - это будет учтено в коде ниже.

A <Item /> не привязан к интервалу карусели Например, один элемент на каждое движение. В один интервал может быть включено несколько элементов, например отображение нескольких статистических данных или графиков в одном представлении. Это карусели, которые следует разработать, и они будут рассмотрены в следующих ниже демонстрациях.

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

Однако, основываясь на событиях прокрутки, мы можем ввести больше UX, чтобы дополнить состояние карусели, например, точки маркера, зафиксированные под каруселью, представляющие каждый интервал, причем точка маркера активного интервала имеет более темный оттенок. Эти дополнительные компоненты UX часто размещаются за пределами самого <ScrollView />, не подвергаясь событиям прокрутки или содержимому с возможностью прокрутки:

// carousel component hierarchy pseudo code
<Container>
  <ScrollView>
     <Item />
     <Item />
     <Item />
      --- next interval
     <Item />
     <Item />
     <Item />   
    </ScrollView>
   <Bullets />
<Container />

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

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

Свойства ScrollView и события прокрутки

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

Изучите следующий пример, в котором выделены ключевые свойства:

// props for a horizontal, paginated ScrollView
<ScrollView
  horizontal={true}
  contentContainerStyle={{ width: `${100 * intervals}%` }}
  showsHorizontalScrollIndicator={false}
  scrollEventThrottle={200}
  decelerationRate="fast"
  pagingEnabled
>
   {/* Items */}
</ScrollView>

В приведенном выше коде мы предполагаем, что каждый <Item /> занимает 100% ширины области карусели, поэтому ширина ScrollView будет 100% умножена на количество элементов.

Два верхних свойства уже были покрыты, выравнивая horizontal содержимое в окне прокрутки, а также определяя его ширину в contentContainerStyle. Разрушение остальных:

  • Параметр showHorizontalScrollIndicator, установленный на false, скрывает полосу прокрутки - горизонтальную полосу, которая появляется, когда пользователь прокручивает, чтобы дать контекст текущей позиции прокрутки. Эта планка не является слишком необходимой в среде карусели, и вместо этого ее можно заменить вышеупомянутым механизмом пули.
  • scrollEventThrottle станет важным в дальнейшем для оптимизации производительности событий прокрутки, установив задержку между запуском событий прокрутки. По крайней мере, для каруселей не требуется поддерживать точное положение прокрутки - данные, полученные из этих событий прокрутки, будут определять, какой интервал отображается только карусель, который можно рассчитать с некоторой задержкой, не влияя на взаимодействие с пользователем.
  • Опора pagingEnabled действует как ярлык для привязки, который останавливает просмотр прокрутки, кратный его размеру при прокрутке. Это то, что эффективно разделяет представление прокрутки на разделы, интеллектуально привязывая положение прокрутки к ближайшему пороговому значению, когда пользователь прокручивает.
  • decelerationRate определяет скорость, с которой прокрутка останавливается после того, как пользователь поднимет палец. Рекомендуется использовать fast для целей карусели, сокращая время, необходимое для перехода к позиции привязки рассматриваемого интервала.

Другие возможности привязки ScrollView

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

Например, pagingEnabled также может быть достигнуто с помощью реквизитов snapToStart, snapToOffsets, snapToInterval и snapToAlignment, которые можно использовать вместе для определения настраиваемой разбивки на страницы в режиме прокрутки. snapToInterval также является сокращением вместо определения массива чисел с помощью snapToOffsets.

snapToStart и snapToOffsets={[0]} достигают одинакового результата.

snapToAlignment - также интересная опора, которая привязывает вид к start, end или center интервала привязки. Подумайте о сценарии, в котором одновременно отображается несколько прокручиваемых окон <Item />. Установка snapToAlignment на center автоматически привяжет активный интервал к центру области прокрутки, следовательно, приоритет будет отдан текущему элементу, находящемуся в центре представления. snapToAlignment можно использовать с snap подпорками и pagingEnabled.

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

Теперь, когда конфигурация ScrollView понята, давайте теперь реализуем компонент <Carousel /> и представим события прокрутки, которые помогут в состоянии карусели и дальнейших дополнениях UX.

Реализация карусели с помощью ScrollView

В этом разделе мы рассмотрим создание полнофункционального <Carousel /> компонента, разделив реализацию на два этапа перед его импортом и использованием.

В компоненте Carousel события ScrollView будут использоваться для запуска обновлений состояния и инициализации компонента в следующем порядке:

  • Шаг 1: Инициализация карусели в событии onContentSizeChange, которое вычислит количество интервалов («страниц» карусели), а также ее ширину.
  • Шаг 2: Расчет текущего активного интервала с событием onScroll и его отражение в окружающем UX.

Будет представлена ​​суть первого шага, а окончательный проект с полным решением будет связан в конце статьи. Посмотреть этот проект сейчас можно здесь, на Github. Вот скринкаст демонстрации, демонстрирующий два стиля карусели:

Обе эти карусели являются производными от одного <Carousel /> компонента, но визуализируют разные компоненты для своих элементов, основываясь на style опоре, которую мы будем передавать в нее.

Импорт компонента Carousel и его свойств

Чтобы понять <Carousel /> компонент, давайте сначала рассмотрим, как мы хотим встроить компонент в JSX - и какие реквизиты для этого требуются:

// importing and using `Carousel`
import Carousel from './Carousel';
export const App = () => ( 
  <View style={styles.container}>
    <Carousel
       style="slides"
       itemsPerInterval={1}
       items={[{
          title: 'Welcome, swipe to continue.',
        }, {
          title: 'About feature X.',
        }, {
          title: 'About feature Y.',
        }]}
     />
  </View>
);

Как видно из вышеизложенного, <Carousel /> поддерживает 3 свойства, каждый из которых играет роль в настройке функций и содержимого карусели:

  • style определяет, какой тип <item /> компонента мы будем использовать для каждого элемента, отображаемого в карусели. В демонстрационном проекте у нас есть два компонента, которые представляют элемент карусели - Stat и Slide. Внутри самой карусели есть оператор switch, который отображает компонент, соответствующий опоре стиля, действуя как простой механизм для изменения внешнего вида или типа карусели для визуализации.
  • itemsPerInterval позволяет нам настроить, сколько компонентов <Item /> отображать в одной прокручиваемой области карусели или сколько элементов отображается одновременно. Переданное здесь значение используется в компоненте для определения количества прокручиваемых секций в карусели. Для стиля slides требуется только 1 элемент на интервал. Однако для стиля stats требуется 3 элемента на интервал, при этом каждый Stat занимает 33% ширины.
  • items - это массив объектов, состоящий из данных каждого элемента карусели. Структура объектов должна быть согласованной и соответствовать требованиям <Item /> компонента к данным. Компонент Slide здесь требует только заголовка, что упрощает требования к элементам.

Эти реквизиты рассматриваются в верхней части Carousel:

// extracting Carousel props
const { items, style } = props;
const itemsPerInterval = props.itemsPerInterval === undefined
  ? 1
  : props.itemsPerInterval;

Отсюда мы можем инициализировать карусель.

Инициализация карусели

Инициализация карусели выполняется в одном из ее обработчиков событий ScrollView, onContentSizeChange. Это событие фактически запускается при первом рендеринге Scroll View, что позволяет нам встроить логику инициализации:

// calling init() within `onContentSizeChange`
...
return (
  <View style={styles.container}>
    <ScrollView
      ...
      onContentSizeChange={(w, h) => init(w)}
    >
     ...
    </ScrollView>
  </View>
)

Если ваш Scroll View изменит размер в зависимости от адаптивного дизайна, onContentSizeChange снова запустится, и размеры карусели будут синхронизированы с init().

onContentSizeChange дает нам ширину и высоту карусели в качестве аргументов обратного вызова, ширина которых затем передается в метод карусели init(). init() отвечает за определение ширины карусели и количества присутствующих интервалов:

// init() implementation
const [intervals, setIntervals] = React.useState(1);
const [width, setWidth] = React.useState(0);
const init = (width: number) => {
    
  // initialise width
  setWidth(width);
  // get total items present
  const totalItems = items.length;
  // initialise total intervals
  setIntervals(Math.ceil(totalItems / itemsPerInterval));
 }

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

Следующий Gist описывает компонент <Carousel /> на этом этапе инициализации:

Представляем расчет текущего интервала

Чтобы вычислить текущий интервал карусели (также называемый «страницей» карусели), нужно ввести немного больше логики в компонент <Carousel />. Во-первых, дополнительный хук useState для хранения текущего интервала в качестве значения по умолчанию 1:

const [interval, setInterval] = React.useState(1);

Для расчета активного интервала мы используем обработчик событий ScrollView onScroll. Когда он сработает, мы вызовем другой метод getInterval(), чтобы определить текущий интервал на основе текущего смещения ScrollView:

// defining onScroll event and getInterval()
const getInterval = (offset: any) => {
  for (let i = 1; i <= intervals; i++) {
    if (offset < (width / intervals) * i) {
      return i;
    }
    if (i == intervals) {
      return i;
    }
  }
}
...
<ScrollView
  ...
  onScroll={data => {
    setInterval(getInterval(data.nativeEvent.contentOffset.x));
  }}
  scrollEventThrottle={200}
>
  ...
</ScrollView>

getInterval() - это просто цикл for с парой условных операторов внутри, проверяющий, находится ли текущее смещение прокрутки между определенными пороговыми значениями интервала. Свойство scrollEventThrottle ограничивает запуск события прокрутки каждые 200 миллисекунд, чтобы не сильно сказываться на производительности.

Добавление дополнительного UX

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

Для динамического построения маркеров можно использовать другой цикл for для создания правильного количества маркеров на основе количества интервалов:

// constructing bullet points
let bullets = [];
for (let i = 1; i <= intervals; i++) {
  bullets.push(
    <Text
      key={i}
      style={{
        ...styles.bullet,
        opacity: interval=== i ? 0.5 : 0.1
      }}
    >
      &bull;
    </Text>
  );
}

Каждая точка маркера заключена в собственный Text компонент, при этом текущий выбранный interval имеет менее интенсивную непрозрачность. После создания массив bullets можно просто встроить в JSX:

// inserting bullets after ScrollView
    
      ...
    </ScrollView>
  <View style={styles.bullets}>
    {bullets}
  </View>
</View>
)

Полную реализацию Carousel можно найти здесь.

В итоге

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

В этой статье показано, как создавать карусели с естественными ощущениями с помощью ScrollView и как построить окружающую логику карусели вокруг компонента. Мы также рассмотрели, как структурировать автономный <Carousel /> компонент, который поддерживает различные стили рабочей области с конкретными компонентами, такими как Stat и Slide.

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

Проект можно просмотреть или клонировать здесь, на Github, с игровым демо, демонстрирующим как статистику, так и карусели в стиле слайд-шоу.