Сложная анимация React Native простым языком

Сложная Анимации React Native простым языком. На русском stepanstepan4@gmail.com
Сложная Анимации React Native простым языком. На русском stepanstepan4@gmail.com

Данная статья должна послужить не только хорошим описанием нативного API анимации React native, но и в какой-то степени шпаргалкой для вас, куда вы сможете обратиться в подходящий момент. Будут рассмотрены основные принципы анимации, анимирование стилевых свойств.

Текст не содержит информации об обработке жестов PanResponder, однако публикация об обработке жестов может повиться в будущем при хорошей оценке текущей статьи.

Сразу хочу упомянуть, что для лучшего понимания анимации в React native будет полезно изучить принцип потоков (thread) на которых основано взаимодействие интерфейса приложения и нативного кода.

Однако можно попробовать сразу перейти к основным принципам анимации.

В статье будут рассмотрены примеры с использованием React hooks и функциональных компонентов и наиболее оптимальные практики по структурированию и разделению кода анимации в хуках.

Содержание:

  1. Animated и Value/ValueXY
  2. Типы анимации
  3. useNativeDriver
  4. Примеры базовой анимации transform / opacity
  5. Примеры базовой анимации ui макета
  6. Как лучше хранить анимацию
  7. Коротко о LayoutAnimation
  8. Reanimated или Animated
  9. Контакты

1. Animated и Value/ValueXY

Animated - это класс / API / инструмент с которым мы работает чтобы

  • получить значение которое мы будем анимировать
  • анимировать полученное значение

Value и ValueXY - это как раз те значения, которые мы будем анимировать. О них нужно думать, как об обычных переменных javascript в браузере.

  • Эти значения нужно инициализировать в теле компонента.
  • В рамках тела функционального компонента React, мы должны использовать useRef , так как переменная должна создаваться один раз при первой отрисовке компонента и не переинициализироваться при последующих перерисовках или изменениях состояния (вместо useRef можно так же использовать useMemo / useState ).
  • Различие между ними только в значении, которое по итогу можно использовать одинаково в большинстве случаев, но для удобства обработки анимации жестов было произведено разделение

В комментариях кода ниже можно увидеть ту аналогию, котороя приведена выше:

import React, { useRef } from "react"; import {Animated} from "react-native"; const App = () => { const value = useRef(new Animated.Value(0)).current //const value = 0 const valueXY = useRef(new Animated.ValueXY({x:0,y:0})).current //const valueXY = {x:0,y:0} return <View/> }

Мы мысленно представляем value и valueXY как переменные, однако не забываем, что у них так же некоторые методы о которых мы поговорим позже.

Именно эти значения переменных мы должны

  • использовать в стилях <Animated.View/> и других анимационных UI компонентах.
  • анимировать, используя инструменты предоставляемые Animated ( spring / timing / modulo / delay / sequence / loop / diffClamp). О них позже.

Они являются основной и единственной единицей для анимации.

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

2. Типы анимации

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

Я постараюсь коротко, так как все необходимая информация по этому разделу содержится в статье блога React Native.

Всю анимацию можно разделить на два типа, по тому свойству, которое мы анимируем.

Первый тип - трансформации элемента через transform и изменение свойств элемента, которые не повлияют на общий flex box макет. (opacity, scale, translateX и тп)

Второй тип - анимация изменения flex box UI макета и изменение цвета. (height, marginTop, backgroundColor, color и тп)

3. useNativeDriver

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

  • true - когда мы работаем с первым типом анимации, и нуждаемся в анимации абсолютных значений, не влияем на flex макет. (это не сработает для анимации других свойств и будет ошибка)
  • false - можно указать в любом случае (это сработает для обоих ситуаций и говорит о том, что анимация не будет обрабатываться нативным драйвером ОС устройства). Подходит для обработки анимации flex макета и цветовых свойств.

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

4. Примеры базовой анимации transform / opacity

Основные шаги по анимации свойств:

  1. Создать переменную анимации (например Animated.Value)
  2. Написать анимационный компонент UI (например Animated.View)
  3. Указать значение стилевого свойства UI компонента равному переменной анимации
  4. Анимируем переменную через инструмент Animated в момент, когда необходимо анимировать свойство UI стиля (например Animated.timing)
import React, { useRef } from 'react'; import { Animated, Button, StyleSheet, View } from 'react-native'; const App = () => { const value = useRef(new Animated.Value(0)).current const startAnimate = () => { Animated.timing(value, { toValue: 100, useNativeDriver: true }).start() } return <Animated.View style={{ transform: [{ translateY: value }], height: 200, width: 200, justifyContent: 'center', backgroundColor: '#4649ad' }} > <Button title={'start animate'} onPress={startAnimate} /> </Animated.View> }

При запуске нажатии на кнопку должен произойти сдвиг элемента на 100 единиц по оси Y в течение 1000 мс.

Теперь можем попробовать разобраться в интерполяции на примере этого простого примера. Допустим, мы хотим, чтобы переменная анимации получала значение равное 100. Но мы не хотим, чтобы наша кнопка сдвигалась на 100 единиц, и для этого мы будем говорить, что при 100 единицах анимации мы будем сдвигать нашу кнопку на 70 единиц:

import React, { useRef } from 'react'; import { Animated, Button, StyleSheet, View } from 'react-native'; const App = () => { const value = useRef(new Animated.Value(0)).current const startAnimate = () => { Animated.timing(value, { toValue: 100, useNativeDriver: true, duration: 1000 }).start() } return <Animated.View style={{ transform: [{ translateY: value.interpolate({ inputRange: [0, 100], outputRange: [0, 70] }) }], height: 200, width: 200, justifyContent: 'center', backgroundColor: '#4649ad' }} > <Button title={'start animate'} onPress={startAnimate} /> </Animated.View> }

Помогает нам в этом interpolate метод, переменной анимации, которому мы говорим о входных значениях от 0 до 100 и о выходных значениях от 0 до 70.

Благодаря данному методу - мы можем создать всего одну переменную анимации и за ее счет анимировать сразу несколько свойств стиля. Допустим opacity и translateX:

import React, { useRef } from 'react'; import { Animated, Button, StyleSheet, View, Easing } from 'react-native'; const App = () => { const animate_state = { start: 0, end: 100 } const value = useRef(new Animated.Value(animate_state.start)).current const startAnimate = () => { Animated.timing(value, { toValue: animate_state.end, useNativeDriver: true, duration: 1000, easing: Easing.bounce }).start() } const inputRange = [animate_state.start, animate_state.end] //или Object.values(animate_state) const translateY = value.interpolate({ inputRange, outputRange: [0, 200] }) const opacity = value.interpolate({ inputRange, outputRange: [1, 0.2] }) return <Animated.View style={{ transform: [{ translateY }], opacity, height: 200, width: 200, justifyContent: 'center', backgroundColor: '#4649ad' }} > <Button title={'start animate'} onPress={startAnimate} /> </Animated.View> }

Теперь у нас уже есть одна переменная анимации, и мы только интерполируем ее и получаем несколько новых анимационных переменных, которые присваиваем свойствам стиля. И при анимации одной переменной - у нас меняются все значения. Для разнообразия так же добавили easing свойство, с которым можно поэкспериментировать, и не забываем о том, почему useNativeDriver = false: мы анимируем абсолютные значения и трансформации.

Предлагаю воспользоваться намного большим перечнем инструментов анимации и зациклить два последовательных действия анимации использую sequence и loop:

import React, { useRef } from 'react'; import { Animated, Button, StyleSheet, View, Easing } from 'react-native'; const App = () => { const animate_state = { start: 0, end: 100 } const value = useRef(new Animated.Value(animate_state.start)).current const startAnimate = () => { Animated.loop( Animated.sequence([ Animated.timing(value, { toValue: animate_state.end, useNativeDriver: true, duration: 1000, easing: Easing.ease }), Animated.timing(value, { toValue: animate_state.start, useNativeDriver: true, duration: 1500, easing: Easing.cubic }), ]) ).start() } const inputRange = [animate_state.start, animate_state.end] //или Object.values(animate_state) const translateY = value.interpolate({ inputRange, outputRange: [0, 200] }) const opacity = value.interpolate({ inputRange, outputRange: [1, 0.2] }) return <Animated.View style={{ transform: [{ translateY }], opacity, height: 200, width: 200, justifyContent: 'center', backgroundColor: '#4649ad' }} > <Button title={'start animate'} onPress={startAnimate} /> </Animated.View> }
stepanstepan4@gmail.com

Готово - важно понять синтаксис анимаций и не забывать о том, что метод start() должен быть только у самой внешней функции анимации - loop.

5. Примеры базовой анимации ui макета

Для этого раздела мы будем указывать useNativeDriver только равный значению false.

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

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

К плюсам можно отнести то, что перед нами открывается огромный спектр свойств которые мы можем анимировать, и тут не хватит места, чтобы их перечислять. Также важно сказать, что мы можем использовать строковые значения для интерполяции, например - мы можем сказать нашей анимации реагировать на входные значения от 0 до 1, а в ответ получать значение ширины блока равное от 20% до 110%

//свойство анимации ширины блока в процентах const width = value.interpolate({ inputRange:[0,1], outputRange: ['20%', '110%'] }) //свойство анимации цвета блока const backgroundColor = value.interpolate({ inputRange:[0,1], outputRange: ['#4649ad', '#f4f4f4'] })

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

import React, { useRef } from 'react'; import { Animated, Button, StyleSheet, View, Easing } from 'react-native'; const Apps = () => { const animate_state = { start: 0, end: 100 } const value = useRef(new Animated.Value(animate_state.start)).current const startAnimate = () => { Animated.timing(value, { toValue: animate_state.end, useNativeDriver: false, duration: 1000 }).start() } const inputRange = Object.values(animate_state) const height = value.interpolate({ inputRange, outputRange: [300, 1000] }) const width = value.interpolate({ inputRange, outputRange: ['100%', '20%'] }) const backgroundColor = value.interpolate({ inputRange, outputRange: ['#4649ad', '#d0f0fd'] }) return <Animated.View style={{ height, width, justifyContent: 'center', backgroundColor }} > <Button title={'start animate'} onPress={startAnimate} /> </Animated.View> }

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

import React, { useRef } from 'react'; import { Animated, Button, StyleSheet, View, Easing } from 'react-native'; const ItemAnimate = ({ index }: { index: string }) => { const animate_state = { start: 0, end: 100 } const value = useRef(new Animated.Value(animate_state.start)).current const startAnimate = () => { Animated.timing(value, { toValue: animate_state.end, useNativeDriver: false, duration: 1000, easing: Easing.bounce }).start() } const inputRange = Object.values(animate_state) const height = value.interpolate({ inputRange, outputRange: [50, 150] }) const backgroundColor = value.interpolate({ inputRange, outputRange: ['#4649ad', '#d0f0fd'] }) return <Animated.View style={{ height, width: '100%', justifyContent: 'center', backgroundColor }} > <Button title={`start animate ${index}`} onPress={startAnimate} /> </Animated.View> } const Apps = () => { const data = new Array(10).fill(1) return <View> {data.map((_, key) => { return <ItemAnimate index={String(key)} key={key} /> })} </View> }

Существует еще несколько удобных инструментов (diffClamp / divide / modulo и тд), но о них лучше говорить в связке с обработкой событий анимации Animated.event и обработкой жестов PanResponder.

6. Как лучше хранить анимацию

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

//Item.tsx const ItemAnimate = ({ index }: { index: string }) => { const { onPress, height, backgroundColor } = useAnimateItemStyle() return <Animated.View style={{ height, width: '100%', justifyContent: 'center', backgroundColor }} > <Button title={`start animate ${index}`} onPress={onPress} /> </Animated.View> } const useAnimateItemStyle = () => { const [isOpen, setIsOpen] = useState(false) const animate_state = { start: 0, end: 100 } const value = useRef(new Animated.Value(animate_state.start)).current const onPress = () => { Animated.timing(value, { toValue: isOpen ? animate_state.start : animate_state.end, useNativeDriver: false, duration: 300, easing: Easing.exp }).start() setIsOpen(!isOpen) } const inputRange = Object.values(animate_state) const height = value.interpolate({ inputRange, outputRange: [50, 150] }) const backgroundColor = value.interpolate({ inputRange, outputRange: ['#4649ad', '#d0f0fd'] }) return { height, backgroundColor, onPress } } //App.tsx const Apps = () => { const data = new Array(10).fill(1) return <View> {data.map((_, key) => { return <ItemAnimate index={String(key)} key={key} /> })} </View> }

Как можно было заметить - вся логика анимации выполняется в useAnimateItemStyle() хуке, которые вынесен отдельно от компонента, что позволяет вообще забыть об анимации при работе с компонентом, а так же позволяет использовать внутри нашей анимации информацию из других мест приложения, например при использовании useSelector(). Чтобы добавить обработку логики пропсов в хук анимации - достаточно передать данные из пропсов в аргумент нашего хука, например useAnimateItemStyle(error_props_value)

7. Коротко о LayoutAnimation

LayoutAnimation - это отличная фича для ленивых и рисковых ребят. Она позволяет автоматически анимировать изменение layout изменения вызванные изменением состояния с перерисовкой. Простыми словами - если у вас было состояние const [height, setHeight] = useState(100) и ваша кнопка меняет высоту со 100 до 200 единиц. Вы просто одновременно с вызовом setHeight(200) вызываете LayoutAnimation.configureNext функцию, которая делает все за вас. Этот подход рискован тем, что существует очень много ситуаций, когда этот подход вызывает абсолютно непредсказуемые баги при анимации различных мест UI, особенно на различных ОС. И для решения этих багов зачастую приходилось приходить к использованию тех методов и инструментов, которые указаны выше.

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

//показательный кусок - как это выполнено в итоговом коде: const menu=useSelector(({menu})=>menu) const onChangePress=(index:number)=>{ const new_menu={...menu} new_menu.splice(index,1) dispatch(setMenuAction(new_menu)) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) }

8. Reanimated или Animated

Речь идет о популярной библиотеке react-native-reanimated

А точнее об ее второй версии, которая привнесла огромный вклад в обработку анимации react native, первую версию не упоминаю, так как ее отличие от нативной анимации Animated мало чем отличается. Вторая версия принесла в мир разработки react native превосходную концепцию хуков для работы с анимацией, которые позволяют писать код еще более компактнее и иметь более широкие спектр возможностей для этого, а также для обработки жестов. Однозначно выбор стоит на стороне reanimated, однако сразу сложно прийти к пониманию всех тех концепций, которые там используются. Но прийти к пониманию нативной анимации мне все таки помогла изначально изучение этих концепций. Также в этом мире существует превосходный ютуб канал, который я готов от всего сердца порекомендовать для изучения всех концепций анимации react native.

9. Контакты

Буду рад пообщаться и ответить на все вопросы
мой гит:
@interhub
телеграм: @Stepan_Turchenko

Степан Турченко
Middle developer
66
Начать дискуссию