Общий переход элемента с React Native

В этом посте я расскажу о том, как достичь перехода общего элемента с помощью React Native как для iOS, так и для Android.

Я разместил код на GitHub, и вы можете взглянуть, если хотите сразу перейти к нему.

Намерение

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

Это гораздо более плавный и непрерывный опыт.

Подход

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

Я передаю информацию об общем элементе - например, его положение и размер - между этими двумя элементами.

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

Таким образом, хотя элемент технически не разделяется, этот хитрый трюк с дымом и зеркалом создает впечатление, что это так.

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

Шаг 1. Анимация входа и выхода

У меня здесь два экрана: сетка и детали. Из экрана сетки мы можем запустить экран деталей, щелкнув одно из изображений в сетке. Затем мы можем вернуться к экрану сетки, нажав кнопку «Назад».

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

Посмотрим, как мы это реализуем.

Вот как выглядит приложение без каких-либо переходов. Нажав на отдельное изображение, вы попадете на экран с подробными сведениями.

Давайте добавим переход к первому экрану сетки. Здесь мы используем простой переход постепенного исчезновения с использованием Animated api, который интерполирует атрибут непрозрачности контейнера экрана сетки от 1 до 0.

Теперь, когда мы это сделали, вот как это выглядит:

Не плохо. Мы видим, что сетка тускнеет, когда мы переходим к экрану деталей.

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

Это делается путем присвоения интерполированного значения Animated свойству translateY текстового контейнера.

А вот как это выглядит:

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

Шаг 2: переходный слой для общего элемента

Теперь мы добавляем переходный слой, который появляется во время перехода и содержит только общий элемент.

Этот слой активируется при щелчке по изображению в сетке. Он получает информацию об общем элементе, такую ​​как его положение и размер, как с экрана сетки, так и с экрана сведений.

Шаг 3: Анимация в переходном слое

У нас есть информация на переходном уровне об исходной и целевой позиции общего элемента. Нам просто нужно их оживить.

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

Путем интерполяции по ширине, высоте, сверху и слева

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

А вот как это выглядит:

Анализ эффективности

При использовании Animated мы объявляем граф узлов, представляющих анимацию, которую мы хотим выполнить, а затем используем драйвер для обновления значения Animated с использованием предопределенной кривой.

Ниже приведены этапы создания анимации и ее места:

  • JavaScript: драйвер анимации использует requestAnimationFrame для выполнения в каждом кадре и обновления значения, которое он управляет, используя новое значение, которое он вычисляет на основе кривой анимации.
  • JavaScript: промежуточные значения вычисляются и передаются в узел реквизита, прикрепленный к View.
  • JavaScript: View обновляется с помощью setNativeProps.
  • JavaScript для собственного моста.
  • Собственный: UIView или android.View обновляется.

Как видите, большая часть работы происходит в потоке JavaScript. Если он заблокирован, анимация будет пропускать кадры. Он также должен проходить через мост JavaScript-Native в каждом кадре для обновления собственных представлений.

Эту проблему можно решить с помощью useNativeDriver. Это перемещает все эти шаги в родной.

Поскольку Animated создает график анимированных узлов, его можно сериализовать и отправить в собственный только один раз при запуске анимации. Это устраняет необходимость обратного вызова в потоке JavaScript. Собственный код может позаботиться об обновлении представлений непосредственно в потоке пользовательского интерфейса в каждом кадре.

Основное ограничение заключается в том, что мы можем анимировать только свойства, не относящиеся к макету. Такие вещи, как transform и opacity, будут работать, но свойства flexbox и position, подобные тем, которые использовались выше, - нет.

Интерполяция при преобразовании и использование useNativeDriver

Давайте теперь оживим с помощью преобразования. Это потребует некоторой математики, чтобы вычислить масштаб, положение x и y.

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

Мы можем получить значение начального масштаба с помощью такой строки JavaScript:

openingScale = sourceDimension.width / destinationDimension.width;

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

const sourceAspectRatio = source.width / source.height;
const destAspectRatio = destination.width / destination.height;
if (aspectRatio - destAspectRatio > 0) {
  // Landscape image
  const newWidth = aspectRatio * destination.height;
  openingScale = source.width / newWidth;
} else {
  // Portrait image
  const newHeight = destination.width / aspectRatio;
  openingScale = source.height / newHeight;
}

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

if (aspectRatio - destAspectRatio > 0) {
  // Landscape image
  destination.pageX -= (newWidth - destinationWidth) / 2;
} else {
  // Portrait image
  destination.pageY -= (newHeight - destinationHeight) / 2;
}

Это идеально! Теперь у нас есть правильный размер и позиция для переходного изображения.

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

const translateInitX = source.pageX + source.width / 2;
const translateInitY = source.pageY + source.height / 2;
const translateDestX = destination.pageX + destination.width / 2;
const translateDestY = destination.pageY + destination.height / 2;

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

const openingInitTranslateX = translateInitX - translateDestX;
const openingInitTranslateY = translateInitY - translateDestY;

С этим найденным начальным масштабом и значениями перевода мы можем анимировать с помощью Animated api.

Вот и все. Теперь у нас работает переход. Теперь мы можем использовать useNativeDriver, так как сейчас мы анимируем только свойства, не относящиеся к макету.

Шаг 4. Скрытие исходного и конечного изображений во время перехода

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

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

Посмотрим теперь на результат.

Шаг 5. Обработайте кнопку "Назад"

Во время перехода к экрану подробностей с помощью Animated.timing() мы меняем AnimatedValue с 0 на 1. Поэтому, когда нажимается кнопка «Назад», нам просто нужно изменить AnimatedValue с 1 на o.

Вот и все. Вы можете проверить код на Github и опробовать демо на Expo.





Также посмотрите трансляцию Эрика Висенти о переходе общих элементов.

Спасибо, что нашли время и прочитали этот пост. Если вы нашли это полезным, пожалуйста, похлопайте и поделитесь им. Вы можете связаться со мной в Twitter @narendra_shetty.