Недавно мне пришлось разработать складной или многослойный липкий заголовок в React Native для проекта, я потратил некоторое время на то, чтобы понять, как я буду его реализовывать, и в то время я бы предпочел найти руководство по достижению того же . Поскольку я не мог найти ни одного, соответствующего моим требованиям. Я решил написать туториал по тому же самому.
Что мы строим
Заголовок скрывается и открывается в зависимости от направления прокрутки, и после остановки прокрутки заголовок удобно привязывается к ближайшему состоянию, то есть наполовину скрыт или полностью раскрыт. Этот эффект можно увидеть в таких приложениях, как WhatsApp, Youtube, Telegram и т. Д. Мы будем использовать Animated API React Native для его создания, так что приступим!
Я сделал стартовый шаблон, который сэкономит нам время, сосредоточив внимание на теме анимации в этой статье. Поэтому я рекомендую вам клонировать репозиторий и следовать :)
Инструкция по настройке стартового шаблона
- Клонировать репозиторий
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
Обзор того, что мы собираемся делать
- Перевод заголовка на основе событий прокрутки
- Реализация привязки к полностью развернутому или полуразвернутому состоянию
У нас есть 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.
- Следуйте за мной, чтобы узнать больше о соответствующем контенте.
- Напоследок поделитесь статьей с друзьями!
Если вам понравилась эта статья, продемонстрируйте свою поддержку, хлопнув 👏 по этой статье, так как это моя первая статья, и это побудит меня писать чаще.