Spec-Zone.ru › React Native

Анимации

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

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

Animated API

API Animated разработан для лаконичного выражения широкого спектра интересных анимационных и интерактивных шаблонов очень эффективным способом. Animated сосредоточен на декларативных взаимосвязях между входными и выходными данными, с настраиваемыми преобразованиями между ними, и start/stop методами для управления выполнением анимации по времени.

Animated экспортирует шесть типов анимируемых компонентов: View, Text, Image, ScrollView, FlatList и SectionList, но вы также можете создать свои собственные, используя Animated.createAnimatedComponent().

Например, контейнерный вид, который плавно появляется при подключении, может выглядеть так:

Давайте разберем, что здесь происходит. В конструкторе FadeInView новый Animated.Value с именем fadeAnim инициализируется как часть state. Свойство opacity на View сопоставлено с этим анимированным значением. За кулисами числовое значение извлекается и используется для установки непрозрачности.

При подключении компонента непрозрачность устанавливается в 0. Затем запускается анимация плавного ускорения/замедления на анимированном значении fadeAnim, которая будет обновлять все зависимые сопоставления (в данном случае, только непрозрачность) на каждом кадре по мере того, как значение анимируется до конечного значения 1.

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

Настройка анимаций

Анимации сильно настраиваются. Можно настраивать пользовательские и предопределенные функции плавного ускорения/замедления, задержки, длительности, коэффициенты затухания, константы пружины и многое другое в зависимости от типа анимации.

Animated предоставляет несколько типов анимаций, наиболее часто используемым из которых является Animated.timing(). Он поддерживает анимацию значения во времени с использованием различных предопределенных функций плавного ускорения/замедления, или вы можете использовать свои собственные. Функции плавного ускорения/замедления обычно используются в анимации для передачи постепенного ускорения и замедления объектов.

По умолчанию timing будет использовать кривую easeInOut, которая передает постепенное ускорение до полной скорости и завершается постепенным замедлением до остановки. Вы можете указать другую функцию плавного ускорения/замедления, передав параметр easing. Также поддерживаются пользовательские duration или даже delay перед началом анимации.

Например, если мы хотим создать анимацию объекта длительностью 2 секунды, который немного отступает назад перед перемещением в конечное положение:

Animated.timing(this.state.xPosition, {
  toValue: 100,
  easing: Easing.back(),
  duration: 2000
}).start();

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

Компоновка анимаций

Анимации могут быть объединены и воспроизведены последовательно или параллельно. Последовательные анимации могут воспроизводиться сразу после завершения предыдущей анимации, или они могут запускаться после указанной задержки. API Animated предоставляет несколько методов, таких как sequence() и delay(), каждый из которых принимает массив анимаций для выполнения и автоматически вызывает start()/stop() при необходимости.

Например, следующая анимация плавно останавливается, а затем возвращается, одновременно вращаясь:

Animated.sequence([
  // decay, then spring to start and twirl
  Animated.decay(position, {
    // coast to a stop
    velocity: { x: gestureState.vx, y: gestureState.vy }, // velocity from gesture release
    deceleration: 0.997
  }),
  Animated.parallel([
    // after decay, in parallel:
    Animated.spring(position, {
      toValue: { x: 0, y: 0 } // return to start
    }),
    Animated.timing(twirl, {
      // and twirl
      toValue: 360
    })
  ])
]).start(); // start the sequence group

Если одна анимация останавливается или прерывается, все остальные анимации в группе также останавливаются. Animated.parallel имеет параметр stopTogether, который можно установить в false для отключения этой функции.

Полный список методов компоновки можно найти в разделе «Компоновка анимаций» справочника API Animated API.

Объединение анимированных значений

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

Есть случаи, когда анимированному значению нужно инвертировать другое анимированное значение для расчета. Например, инвертирование масштаба (2x --> 0,5x):

const a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
  toValue: 2
}).start();

Интерполяция

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

Базовое отображение для преобразования диапазона 0-1 в диапазон 0-100:

value.interpolate({
  inputRange: [0, 1],
  outputRange: [0, 100]
});

Например, вы можете захотеть, чтобы ваше Animated.Value менялось от 0 до 1, но анимировать положение от 150 пикселей до 0 пикселей, а непрозрачность от 0 до 1. Это можно сделать, изменив style из приведенного выше примера следующим образом:

  style={{
    opacity: this.state.fadeAnim, // Binds directly
    transform: [{
      translateY: this.state.fadeAnim.interpolate({
        inputRange: [0, 1],
        outputRange: [150, 0]  // 0 : 150, 0.5 : 75, 1 : 0
      }),
    }],
  }}

interpolate() также поддерживает несколько сегментов диапазона, что полезно для определения «мертвых зон» и других полезных трюков. Например, чтобы получить отношение отрицания в -300, которое переходит в 0 в -100, затем возвращается к 1 в 0, а затем возвращается к нулю в 100, а затем «мертвую зону», которая остается на 0 для всего, что выходит за эти пределы, вы могли бы сделать:

value.interpolate({
  inputRange: [-300, -100, 0, 100, 101],
  outputRange: [300, 0, 1, 0, 0]
});

Что соответствовало бы следующим преобразованиям:

Input | Output
------|-------
  -400|    450
  -300|    300
  -200|    150
  -100|      0
   -50|    0.5
     0|      1
    50|    0.5
   100|      0
   101|      0
   200|      0

interpolate() также поддерживает отображение в строки, позволяя анимировать цвета, а также значения с единицами. Например, если вы хотите анимировать поворот, вы можете сделать:

value.interpolate({
  inputRange: [0, 360],
  outputRange: ['0deg', '360deg']
});

interpolate() также поддерживает произвольные функции плавного ускорения/замедления, многие из которых уже реализованы в модуле Easing. interpolate() также имеет настраиваемое поведение для экстраполяции outputRange. Вы можете установить экстраполяцию, задав параметры extrapolate, extrapolateLeft, или extrapolateRight. Значение по умолчанию равно extend, но вы можете использовать clamp, чтобы предотвратить превышение выходного значения outputRange.

Отслеживание динамических значений

Анимированные значения также могут отслеживать другие значения, устанавливая toValue анимации на другое анимированное значение вместо простого числа. Например, анимацию «Chat Heads», такую как используется в Messenger на Android, можно реализовать с помощью spring(), привязанного к другому анимированному значению, или с timing() и duration равным 0 для жесткого отслеживания. Они также могут быть объединены с интерполяциями:

Animated.spring(follower, { toValue: leader }).start();
Animated.timing(opacity, {
  toValue: pan.x.interpolate({
    inputRange: [0, 300],
    outputRange: [1, 0]
  })
}).start();

Анимированные значения leader и follower были бы реализованы с помощью Animated.ValueXY(). ValueXY — удобный способ работы с двумерными взаимодействиями, такими как перемещение или перетаскивание. Это базовая оболочка, содержащая два экземпляра Animated.Value и некоторые вспомогательные функции, которые вызывают их, что делает ValueXY прямой заменой Value во многих случаях. Это позволяет отслеживать как x, так и y значения в примере выше.

Отслеживание жестов

Жесты, такие как перемещение или прокрутка, и другие события могут быть непосредственно сопоставлены с анимированными значениями, используя Animated.event. Это делается с помощью структурированной синтаксической записи сопоставлений, чтобы значения можно было извлечь из сложных объектов событий. Первый уровень — массив, чтобы разрешить сопоставление через несколько аргументов, и этот массив содержит вложенные объекты.

Например, при работе с горизонтальными жестами прокрутки вы выполните следующее, чтобы сопоставить event.nativeEvent.contentOffset.x с scrollX (Animated.Value) :

 onScroll={Animated.event(
   // scrollX = e.nativeEvent.contentOffset.x
   [{ nativeEvent: {
        contentOffset: {
          x: scrollX
        }
      }
    }]
 )}

Следующий пример реализует горизонтальный карусель, где индикаторы положения прокрутки анимированы с использованием Animated.event в ScrollView

Пример ScrollView с анимированным событием

  • Компонент функции
  • Компонент класса

При использовании PanResponder, вы могли бы использовать следующий код для извлечения x и y координат из gestureState.dx и gestureState.dy Мы используем null в первом элементе массива, поскольку нас интересует только второй аргумент, переданный обработчику PanResponder, который является gestureState

onPanResponderMove={Animated.event(
  [null, // ignore the native event
  // extract dx and dy from gestureState
  // like 'pan.x = gestureState.dx, pan.y = gestureState.dy'
  {dx: pan.x, dy: pan.y}
])}

Пример PanResponder с анимированным событием

  • Компонент функции
  • Компонент класса

Реагирование на текущее значение анимации

Вы можете заметить, что нет явного способа прочитать текущее значение во время анимации. Это связано с тем, что значение может быть известно только в нативном исполнении из-за оптимизаций. Если вам нужно запустить JavaScript в ответ на текущее значение, есть два подхода:

  • spring.stopAnimation(callback) остановит анимацию и вызовет callback с конечным значением. Это полезно при создании переходов жестов.
  • spring.addListener(callback) вызовет callback асинхронно во время выполнения анимации, предоставив недавнее значение. Это полезно для запуска изменений состояния, например, привязки боба к новому варианту по мере приближения пользователем, поскольку эти более крупные изменения состояния менее чувствительны к нескольких кадрам задержки по сравнению с непрерывными жестами, такими как панорамирование, которые должны выполняться со скоростью 60 кадров в секунду.

Animated разработан для полной сериализации, чтобы анимации могли выполняться с высокой производительностью независимо от обычной JavaScript-очереди событий. Это влияет на API, поэтому имейте это в виду, когда кажется, что что-то сделать немного сложнее, чем в полностью синхронной системе. Посмотрите на Animated.Value.addListener как способ обойти некоторые из этих ограничений, но используйте его умеренно, поскольку в будущем это может сказаться на производительности.

Использование нативного драйвера

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

Использование родного драйвера для обычных анимаций простое. Вы можете добавить useNativeDriver: true в конфигурацию анимации при её запуске.

Animated.timing(this.state.animatedValue, {
  toValue: 1,
  duration: 500,
  useNativeDriver: true // <-- Add this
}).start();

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

Родной драйвер также работает с Animated.event. Это особенно полезно для анимаций, которые следуют за положением прокрутки, так как без родного драйвера анимация всегда будет выполняться с отставанием в одном кадре из-за асинхронной природы React Native.

<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
  scrollEventThrottle={1} // <-- Use 1 here to make sure no events are ever missed
  onScroll={Animated.event(
    [
      {
        nativeEvent: {
          contentOffset: { y: this.state.animatedValue }
        }
      }
    ],
    { useNativeDriver: true } // <-- Add this
  )}>
  {content}
</Animated.ScrollView>

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

Ограничения

Не все, что можно сделать с Animated в настоящее время поддерживается родным драйвером. Основное ограничение заключается в том, что вы можете анимировать только свойства, не связанные с макетом: такие как transform и opacity будут работать, но свойства Flexbox и позиционирования — нет. При использовании Animated.event, оно будет работать только с прямыми событиями, а не с событиями, которые «всплывают». Это означает, что оно не работает с PanResponder, но работает с такими элементами, как ScrollView#onScroll.

Когда анимация выполняется, она может предотвращать рендеринг дополнительных строк компонентов VirtualizedList. Если вам нужно запустить длительную или циклическую анимацию, пока пользователь прокручивает список, вы можете использовать isInteraction: false в конфигурации анимации, чтобы предотвратить эту проблему.

Учитывайте

При использовании стилей преобразования, таких как rotateY, rotateX и других, убедитесь, что стиль преобразования perspective установлен. В настоящее время некоторые анимации могут не отображаться на Android без него. Пример ниже.

<Animated.View
  style={{
    transform: [
      { scale: this.state.scale },
      { rotateY: this.state.rotateY },
      { perspective: 1000 } // without this line this Animation will not render on Android while working fine on iOS
    ]
  }}
/>

Дополнительные примеры

Приложение RNTester содержит различные примеры Animated в использовании:

  • AnimatedGratuitousApp
  • NativeAnimationsExample

LayoutAnimation API

LayoutAnimation позволяет вам глобально настроить анимации create и update, которые будут использоваться для всех представлений в следующем цикле рендеринга/макета. Это полезно для выполнения обновлений макета Flexbox без необходимости измерения или расчета конкретных свойств для их прямой анимации, и особенно полезно, когда изменения макета могут повлиять на предков, например, расширение «показать больше», которое также увеличивает размер родителя и сдвигает строку ниже, которая в противном случае потребовала бы явного согласования между компонентами для одновременной анимации.

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

Обратите внимание, что для работы на Android необходимо установить следующие флаги через UIManager:

UIManager.setLayoutAnimationEnabledExperimental &&
  UIManager.setLayoutAnimationEnabledExperimental(true);

В этом примере используется предопределенное значение, вы можете настроить анимации по своему усмотрению. Смотрите LayoutAnimation.js для получения дополнительной информации.

Дополнительные заметки

requestAnimationFrame

requestAnimationFrame — это полифил из браузера, с которым вы, возможно, знакомы. Он принимает в качестве единственного аргумента функцию и вызывает её перед следующим перерисовыванием. Это важный строительный блок для анимаций, который лежит в основе всех JavaScript-анимационных API. Как правило, вам не нужно вызывать его самостоятельно — API анимации будут управлять обновлениями кадра за вас.

setNativeProps

Как упоминалось в разделе «Прямое управление», setNativeProps позволяет нам изменять свойства компонентов с нативной поддержкой (компоненты, которые на самом деле поддерживаются нативными представлениями, в отличие от составных компонентов) напрямую, без необходимости setState и перерисовки иерархии компонентов.

Мы могли бы использовать это в примере Rebound для обновления масштаба — это может быть полезно, если компонент, который мы обновляем, глубоко вложен и не был оптимизирован с помощью shouldComponentUpdate.

Если вы обнаружите, что ваши анимации пропускают кадры (работают медленнее, чем 60 кадров в секунду), попробуйте оптимизировать их с помощью setNativeProps или shouldComponentUpdate. Или вы можете запустить анимации в потоке пользовательского интерфейса, а не в потоке JavaScript с параметром useNativeDriver. Вы также можете отложить любые вычисления до завершения анимации, используя InteractionManager. Вы можете отслеживать частоту кадров с помощью инструмента «Монитор FPS» в меню разработчика приложения.

© 2022 Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://reactnative.dev/docs/animations

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API