Когда я только начал работать с React Native (RN), у меня никогда не доходили руки до изучения анимации. Многие вещи казались важнее, поэтому я забывал про эту замечательную тему. Но анимации довольно важны, ведь хорошо выглядящему приложению простительны некоторые недостатки.
Когда вы привыкнете к работе анимаций, вам станет проще добавлять их к проектам. Вы получите базовые знания, которые помогут вывести ваши приложения на новый уровень.
В этой статье мы сделаем эту анимацию: рассмотрим ее подробно и увидим, насколько просто она реализована в RN.
Итоговая анимацияДавайте пойдем с самого начала. Запустим $ react-native-init animations
в графическом интерфейсе react native, чтобы запустить проект. Мы увидим этот экран:
Первый шаг — создание простого сценария. Избавьтесь от всего шаблонного кода, чтобы начать с пустой страницы.
import React, { Fragment } from 'react';
import { SafeAreaView, StatusBar, StyleSheet } from 'react-native';
const App = () => {
return (
<Fragment>
<StatusBar
backgroundColor="transparent"
barStyle="dark-content"
translucent={true}
/>
<SafeAreaView>
</SafeAreaView>
</Fragment>
);
};
const styles = StyleSheet.create({
});
export default App;
Теперь добавляем небо, траву и дорогу:
import React, { Fragment } from 'react';
import { Animated, SafeAreaView, StatusBar, StyleSheet, View } from 'react-native';
const App = () => {
return (
<Fragment>
<StatusBar
backgroundColor="transparent"
barStyle="dark-content"
translucent={true}
/>
<SafeAreaView style={styles.background}>
<View style={styles.grass}>
<View style={styles.road}>
<View style={styles.stripes}>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
</View>
</View>
</View>
</SafeAreaView>
</Fragment>
);
};
const styles = StyleSheet.create({
background: {
backgroundColor: '#87CEEB',
flex: 1
},
grass: {
backgroundColor: '#4DED33',
bottom: 0,
position: 'absolute',
height: '70%',
width: '100%'
},
road: {
backgroundColor: '#666560',
height: 160,
justifyContent: 'center',
marginTop: '10%',
width: '100%'
},
stripe: {
backgroundColor: '#FFFFFF',
width: '5%',
height: 10
},
stripes: {
justifyContent: 'space-between',
flexDirection: 'row'
},
});
export default App;
Вот что получилось:
Скриншот экрана с небом, дорогой и травойХороший сценарий! Давайте добавим в проект два изображения машинок:
Голубая машинка Красная машинкаЯ добавил эти изображения в директорию “resources” в корне. Теперь нам нужно добавить их в код. По умолчанию RN может анимировать шесть компонентов: <View>
, <Text>
, <Image>
, <ScrollView>
, <FlatList>
и <SectionList>
. Мы используем компонент<Image>
. Но вместо простой версии возьмем анимированную, <Animated.Image>
, чтобы позднее добавить немного магии. Теперь код дороги выглядит так:
<View style={styles.road}>
<Animated.Image
resizeMode='center'
source={require('./resources/car_blue.png')}
style={{...styles.carImage, ...styles.upperCar}}
/>
<View style={styles.stripes}>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
<View style={styles.stripe}/>
</View>
<Animated.Image
resizeMode='center'
source={require('./resources/car_red.png')}
style={{...styles.carImage, ...styles.lowerCar}}
/>
</View>
С этими стилями:
carImage: {
height: 80,
position: 'absolute',
width: 160
},
lowerCar: {
bottom: 10
},
upperCar: {
top: -20
}
Теперь у нас есть базовое изображение:
Скриншот сценария с машинкамиТеперь давайте разберемся, как анимировать машинки. Используем для этого React Hooks. Простейшая анимация здесь — движение одной из машинок вправо. Начнем с добавления useEffect
и useState
к импортам React, чтобы следить за анимацией, используя состояние:
Теперь нужно добавить переменную, с помощью которой мы будем следить за состоянием анимации. Эта переменная будет получать постепенные обновления анимации от 0% до 100%, в соответствии с установленной продолжительностью. В нашем случае анимируем нижнюю машинку, чтобы она поехала вправо. Мы собираемся пройти от left: 0
до left: 100%
.Объявляем переменную:
Как вы могли догадаться, функция Animated.Value
используется для установки значений, которые будут использоваться в анимации. В переменной хранится не само значение, а функция, похожая на промис, которая разрешается в значение. Это гарантирует, что все будет работать в соответствии с временем анимации.
Теперь нужно использовать хук useEffect
, который гарантирует, что анимация запустится только после монтирования компонента. В данном случае это аналогично использованию componentDidMount
(если у вас есть компонент класса). Внутри него вызовем функцию Animated.timing
, которая обеспечивает постепенное изменения от одного значения к другому. Animated.timing()
принимает два параметра:
lowerCarLeft
.Сейчас вам не стоит беспокоиться о функции замедления, так как у функции timing()
есть значения по умолчанию. После конфигурации анимацию можно запустить. Просто добавьте вызов функции start()
в timing()
. Код будет выглядеть так:
useEffect(() => {
Animated.timing(lowerCarLeft, {
toValue: 100,
duration: 2000
}).start();
}, []);
Код выше постепенно увеличивает переменную lowerCarLeft
в течение 2 секунд (2000 миллисекунд), пока значение переменной не достигнет 100.
Теперь нужно использовать это значение в стиле машинки. Мы анимируем левую позицию нижней машинки, и по мере увеличения значения левой позиции машинка будет двигаться вправо. Получаем результат, добавив left: lowerCarLeft
в свойство стиля:
style={{
...styles.carImage,
...styles.lowerCar,
left: lowerCarLeft
}}
Машинка двигается, но останавливается в неправильной позиции. Так как переданное значение является числом, RN понимает его как пиксели, в итоге выдавая неверный результат. Нам же нужно получить значение в процентах, ведь мы хотим переместить машинку не на 100 пикселей вправо, а на 100 процентов размера экрана. Всегда помните, что использование пикселей может сломать анимацию на экранах разных размеров.
Если попытаться добавить “%” в конец объявления стиля, например left: lowerCarLeft+'%'
(или другие вариации), это не сработает, потому что, как я говорил ранее, переменная хранит не само значение, а функцию, похожую на промис. Строка просто не может быть присоединена к такой функции.
Если попытаться анимировать значение напрямую как строку (изменив объявление переменной и toValue
внутри timing()
с “0%” до “100%”), это тоже не сработает. React Animated не может анимировать строку, так как невозможно определить путь от точки 0 (начало) до точки 100 (конец), если вы работаете не с числами.
И все же существует решение этой проблемы — это “линейная интерполяция” — фича, которая позволяет сопоставлять введенные данные с различными выведенными данными. В нашем случае мы сможем сказать: точка 0 представляет строку “0%” и точка 100 представляет строку “100%”. Но сопоставления могут быть любыми — точка 0 представляет строку “0 градусов”, и точка 100 представляет строку “360 градусов” и так далее.
Давайте посмотрим, как мы делаем интерполяцию в React. Нужно получить анимируемое значение и вызвать для него функцию interpolate()
, передавая массив введенных значений и массив выведенных значений:
lowerCarLeft.interpolate({
inputRange: [0, 100],
outputRange: ['0%', '100%']
})
В точности это означает следующее: когда lowerCarLeft
находится в точке 0, функция interpolate()
возвращает ‘0%’, и так происходит в любой другой точке.
Теперь нужно заменить style
в изображении. Получим такой код:
style={{
...styles.carImage,
...styles.lowerCar,
left: lowerCarLeft.interpolate({
inputRange: [0, 100],
outputRange: ['0%', '100%']
})
}}
К этому моменту нижняя машинка уже анимирована:
Анимация красной машинкиТеперь нужно повторить то же самое для верхней машинки, только в противоположном направлении. Машинка должна начать движение слева в 100% и двигаться до 0%. Готовы?
Что можно заметить:
Если попытаться изменить интерполяцию inputRange: [0, 100]
на [100, 0]
, также изменив начальную точку слева на 100% и конечную точку на 0%, это не сработает. Имейте в виду — при интерполяции вы не обращаетесь к значениям анимации, вы обращаетесь к точке во времени в анимации. [100, 0]
означает, что анимация будет происходить задом наперед, поэтому метод интерполяции ее не принимает. Если вы оставите [0, 100]
, интерполяция будет знать, что в точке 0 должно быть возвращено значение ‘100%’, а в точке 100 — значение ‘0%’.
Также вы заметите, что верхняя машинка завершает анимацию, пока все еще присутствует на экране, в то время как нижняя машинка исчезает. Это просто вопрос оформления. Чтобы избежать этого и, возможно, улучшить движение, давайте изменим конечную точку для верхней машинки и начальную точку для нижней машинки на -50 вместо 0.
Вот что мы получим:
Анимация обеих машинокНаша анимация уже выглядит классно. Нам осталось реализовать бесконечный цикл, чтобы машинки никогда не останавливались.
Давайте напишем его! Нам нужно запустить анимации вместе. Вызываем start()
дважды: по одному разу для каждой анимации, чтобы они запускались самостоятельно. Нам нужно заставить их работать вместе при помощи только одного вызова метода. Для этой задачи React-Native Animated предоставляет функцию parallel()
(другие функции здесь). В нее можно передать параметр, который является массивом всех анимируемых значений, затем вызвать в цепочке start()
, тогда анимации запустятся вместе. Вот так:
Animated.parallel([
Animated.timing(lowerCarLeft, {
toValue: 100,
duration: 2000
}),
Animated.timing(upperCarLeft, {
toValue: -50,
duration: 2000
})
]).start();
Должно сработать. Теперь нужно запустить анимацию снова после окончания. Здесь пригодится трюк с методом start()
— он может получать обратный вызов, выполняемый, когда анимация заканчивается. Мы можем повторить код анимации для повторного запуска, но вместо копирования того же самого кода давайте создадим метод, разделяющий эту логику, чтобы просто вызвать его. Я сделал это так:
let [lowerCarLeft] = useState(new Animated.Value(-50)),
[upperCarLeft] = useState(new Animated.Value(100)),
runAnimation = () => {
Animated.parallel([
Animated.timing(lowerCarLeft, {
toValue: 100,
duration: 2000
}),
Animated.timing(upperCarLeft, {
toValue: -50,
duration: 2000
})
]).start();
};
useEffect(() => {
runAnimation();
});
Теперь внутри start()
просто вызовем runAnimation()
:
runAnimation = () => {
Animated.parallel([
Animated.timing(lowerCarLeft, {
toValue: 100,
duration: 2000
}),
Animated.timing(upperCarLeft, {
toValue: -50,
duration: 2000
})
]).start(() => runAnimation());
};
Ошибок нет, но не работает. Почему? Потому что нужно снова задать анимируемые значения!
runAnimation = () => {
lowerCarLeft.setValue(-50);
upperCarLeft.setValue(100);
Animated.parallel([
Animated.timing(lowerCarLeft, {
toValue: 100,
duration: 2000
}),
Animated.timing(upperCarLeft, {
toValue: -50,
duration: 2000
})
]).start(() => runAnimation());
};
Вот и все! Теперь работает идеально!
Анимация полностью работаетГотово!
Простор для улучшений огромен. Можно задать константы для начального и конечного значений, чтобы не приходилось искать их в коде для изменения. Также можно использовать stagger()
(вместо parallel()
) или запускать работу в случайное время, чтобы избежать роботизированности.
Итоги:
Interpolate
сопоставляет введенные данные с различными выводами (полезно при анимировании строк).parallel()
— это способ запустить несколько анимаций вместе. start()
получает обратный вызов, который вызывается по завершении всех анимаций (полезно для создания бесконечных анимаций).Этого достаточно, чтобы начать самостоятельно создавать простые анимации.
Перевод статьи Mauricio Luis Comin Araldi: The first step into React Native Animations
Комментарии