Перейти к содержанию

Анимации

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

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-секундную анимацию объекта, который слегка отступает назад, прежде чем переместиться в конечное положение:

1
2
3
4
5
6
Animated.timing(this.state.xPosition, {
    toValue: 100,
    easing: Easing.back(),
    duration: 2000,
    useNativeDriver: true,
}).start();

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

Составление анимации

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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,
        useNativeDriver: true,
    }),
    Animated.parallel([
        // after decay, in parallel:
        Animated.spring(position, {
            toValue: { x: 0, y: 0 }, // return to start
            useNativeDriver: true,
        }),
        Animated.timing(twirl, {
            // and twirl
            toValue: 360,
            useNativeDriver: true,
        }),
    ]),
]).start(); // start the sequence group

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

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

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

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

Бывают случаи, когда анимированное значение должно инвертировать другое анимированное значение для расчета. Примером может служить инвертирование шкалы (2x → 0,5x):

1
2
3
4
5
6
7
const a = new Animated.Value(1);
const b = Animated.divide(1, a);

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

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

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

Основное отображение для преобразования диапазона 0-1 в диапазон 0-100 будет таким:

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

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

1
2
3
4
5
6
7
8
9
  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 для всего, что находится за пределами этого диапазона, вы можете сделать следующее:

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

Это будет выглядеть следующим образом:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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() также поддерживает отображение на строки, позволяя вам анимировать цвета, а также значения с единицами измерения. Например, если вы хотите анимировать вращение, вы можете сделать следующее:

1
2
3
4
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, для жесткого отслеживания. Они также могут быть составлены с помощью интерполяций:

1
2
3
4
5
6
7
8
Animated.spring(follower, { toValue: leader }).start();
Animated.timing(opacity, {
    toValue: pan.x.interpolate({
        inputRange: [0, 300],
        outputRange: [1, 0],
        useNativeDriver: true,
    }),
}).start();

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

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

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

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

1
2
3
4
5
6
7
8
9
 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.

1
2
3
4
5
6
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 разработан так, чтобы быть сериализуемым. Используя Native driver, мы отправляем все данные об анимации в native перед началом анимации, что позволяет native-коду выполнять анимацию в потоке UI без необходимости проходить через мост на каждом кадре. После запуска анимации поток JS может быть заблокирован без ущерба для анимации.

Использовать родной драйвер для обычных анимаций можно, установив useNativeDriver: true в конфигурации анимации при ее запуске. Анимации без свойства useNativeDriver по умолчанию будут иметь значение false по унаследованным причинам, но будут выдавать предупреждение (и ошибку проверки типов в TypeScript).

1
2
3
4
5
Animated.timing(this.state.animatedValue, {
    toValue: 1,
    duration: 500,
    useNativeDriver: true, // <-- Set this to true
}).start();

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<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 } // <-- Set this to true
    )}
>
    {content}
</Animated.ScrollView>

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

Предостережения

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

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

Имейте в виду

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

1
2
3
4
5
6
7
8
9
<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:

LayoutAnimation API

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

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

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

1
UIManager.setLayoutAnimationEnabledExperimental(true);

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

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

requestAnimationFrame

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

setNativeProps

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

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

Если вы обнаружите, что ваши анимации падают (частота кадров ниже 60 в секунду), попробуйте использовать setNativeProps или shouldComponentUpdate для их оптимизации. Или вы можете запустить анимацию в потоке UI, а не в потоке JavaScript с опцией useNativeDriver. Вы также можете отложить любую работу, требующую больших вычислений, до завершения анимации, используя InteractionManager. Вы можете контролировать частоту кадров с помощью инструмента "FPS Monitor" в меню In-App Dev Menu.

Ссылки

Комментарии