Недавно мне пришлось разработать складной или многослойный липкий заголовок в React Native для проекта, я потратил некоторое время на то, чтобы понять, как я буду его реализовывать, и в то время я бы предпочел найти руководство по достижению того же . Поскольку я не мог найти ни одного, соответствующего моим требованиям. Я решил написать туториал по тому же самому.

Что мы строим

Заголовок скрывается и открывается в зависимости от направления прокрутки, и после остановки прокрутки заголовок удобно привязывается к ближайшему состоянию, то есть наполовину скрыт или полностью раскрыт. Этот эффект можно увидеть в таких приложениях, как WhatsApp, Youtube, Telegram и т. Д. Мы будем использовать Animated API React Native для его создания, так что приступим!

Я сделал стартовый шаблон, который сэкономит нам время, сосредоточив внимание на теме анимации в этой статье. Поэтому я рекомендую вам клонировать репозиторий и следовать :)

Инструкция по настройке стартового шаблона

  1. Клонировать репозиторий
git clone https://github.com/frzkn/rn-collapsible-header

2. Установка зависимостей

cd rn-collapsible-header && yarn

3. Переключитесь на стартерную ветвь.

git checkout starter

4. Запуск сборщика пакетов метро.

yarn start

4. Запускаем его на устройстве (в моем случае Android)

yarn android

Примечание. В этом руководстве основное внимание уделяется Android, но для iOS оно не сильно отличается.

Откройте App.js и посмотрите, что для нас уже сделано.

App.js

Обзор того, что мы собираемся делать

  1. Перевод заголовка на основе событий прокрутки
  2. Реализация привязки к полностью развернутому или полуразвернутому состоянию

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

Перевод заголовка на основе событий прокрутки

Я разбил следующий процесс на 5 этапов следующим образом:

1. Преобразование наших компонентов в анимированные компоненты

В настоящее время мы используем FlatList, который предоставляет нам React native. Чтобы использовать анимацию, API анимации требует, чтобы мы обернули наши компоненты функцией createAnimatedComponent, которая применяет все свойства анимации к нашему обычному компоненту, пример ниже.

const AnimatedFlatList = createAnimatedComponent(FlatList);

Для наиболее часто используемых компонентов, таких как View, FlatList и т. Д. Animated API уже предоставляет нам эти компоненты, поэтому мы можем начать использовать эти компоненты напрямую. Сначала импортируйте Animated из react-native

import {Animated, ... } from ‘react-native’

Замените наш FlatList на Animated.FlatList, а представление, которое оборачивает компонент заголовка, на Animated.View.

Теперь эти компоненты готовы обрабатывать анимацию и анимированные события.

2. Добавление события onScroll

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

const scrollY = useRef(new Animated.Value(0));

FlatList принимает свойство onScroll, которое постоянно запускается при прокрутке FlatList, мы передаем прокрутку дескриптора функции, которая является анимированным событием. Это анимированное событие выполняет одну задачу: отслеживает изменения scrollY на прокрутке и присваивает их переменной scrollY.

const handleScroll = Animated.event(
  [
    {
      nativeEvent: {
        contentOffset: {y: scrollY.current},
      },
    },
  ],
  {
    useNativeDriver: true,
  },
);

Обратите внимание на ключ useNativeDriver, это очень важно, поскольку он гарантирует, что анимация выполняется в собственном потоке пользовательского интерфейса и не блокирует какие-либо операции Javascript. Это хорошо объясняется в блоге на сайте react-native. Но в двух словах, благодаря этому ваша анимация достигает 60 кадров в секунду даже на устройствах более низкого уровня.

Теперь мы получим Значение, начиная с 0 и заканчивая общей высотой FlatList, когда пользователь прокручивает. Чтобы зафиксировать это значение в диапазоне, мы будем использовать функцию под названием diffClamp.

3. diffЗажимаем наши ценности

Как следует из названия, функция выполняет две функции: фиксирует значения между диапазоном и возвращает новое значение относительно предыдущего значения. Это означает, что для диапазона от 0 до 10, учитывая значение x, мы впервые получаем 5. Нельзя сказать, что последующие вызовы с одним и тем же входом вернут тот же результат.

const scrollYClamped = diffClamp(scrollY.current, 0, headerHeight);

Это вернет нам значение в диапазоне от 0 до высоты заголовка, равной 58 * 2. Наконец, нам нужно интерполировать это значение в диапазон (-headerHeight / 2), т. Е. Половину. расширенное состояние и 0 т. е. полностью развернутое состояние.

4. Интерполяция значения

Идея состоит в том, чтобы перевести заголовок в отрицательном направлении Y в соответствии с позицией прокрутки, поскольку мы хотим перевести его до точки - (headerHeight / 2), которая является одним из выходных диапазонов вместе с 0, что означает, что заголовок вообще не переводится.

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

const translateY = scrollYClamped.interpolate({
 inputRange: [0, headerHeight],
 outputRange: [0, -(headerHeight / 2)],
 });
const translateYNumber = useRef();
translateY.addListener(({value}) => {
  translateYNumber.current = value;
});

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

Мы также добавляем прослушиватель к этому значению, поскольку в следующей части нам потребуется Animated.Value в типе Number.

5. Последний кусок пазла

Перейдите к Animated.View оболочке, с которой мы столкнулись ранее, и добавьте следующий стиль

<Animated.View style={[styles.header, {transform: [{translateY}]}]}>
 <Header {…{headerHeight}} />
 </Animated.View>

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

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

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

  • Пример WhatsApp Messenger

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

  • Пример Telegram Messenger

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

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

Добавление события onMomentumScrollEnd

В отличие от события onScroll, событие onMomemtumScrollEnd срабатывает только после того, как пользователь прекращает прокрутку. так что давайте начнем с написания функции handleSnap и использования ссылки, назначенной нашему FlatList.

<Animated.FlatList
 scrollEventThrottle={16}
 contentContainerStyle={{paddingTop: headerHeight}}
 onScroll={handleScroll}
 ref={ref}
 onMomentumScrollEnd={handleSnap}
 data={data}
 renderItem={ListItem}
 keyExtractor={(item, index) => `list-item-${index}-${item.color}`}
 />

Этот блок также довольно понятен, translateYNumber - это Animated.Value, преобразованный в тип Number. Мы используем это значение, чтобы проверить, находится ли заголовок в желаемом месте, в противном случае мы прокручиваем FlatList достаточно, чтобы убедиться, что заголовок привязан к желаемому месту.

Примечание. Теоретически нам не нужно добавлять смещениеY в ключ смещения метода scrollToOffset, но, похоже, это ошибка, когда смещение не применяется автоматически.

И ура! наши результаты намного лучше, чем раньше, они показывают нам, что что-то настолько простое может улучшить или испортить пользовательский опыт. Я очень доволен результатами, и на этом все :)

Как вы можете внести свой вклад?

  • Сделав пиар в репозитории, я знаю, что есть намного лучшие способы сделать то же самое, и я хотел бы увидеть ваши подходы.
  • Свяжитесь со мной в Twitter, Linkedin или GitHub.
  • Пометьте репозиторий Github.
  • Следуйте за мной, чтобы узнать больше о соответствующем контенте.
  • Напоследок поделитесь статьей с друзьями!

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