react native 에는 animation 을 추가할 수 있습니다.
이때 animation 을 도와주는 기능이 react-native 에 자체적으로 가지고 있습니다.
import { Animated } from 'react-native'
Animated 를 사용할 때 꼭 지켜야 하는 규칙이 있습니다.
만약 이동시켜야하는 값인 어떤 value 값이 필요하다면 useState 를 쓰면 안되고
animated.value 를 사용해야 합니다. 꼭 AnimatedAPI 를 사용해야 합니다.
let x = new Animated.Value(0)
두번째로 Animated.Value 값은 절대 직접 수정하지 않습니다.
let x = new Animated.Value(0) <-- O
let x = 0 <-- X
마지막 규칙으로는 Animateable Components 에서만 animation 을 줄 수 있습니다.
이 말은 아무것에나 animation 을 줄 수 없다는 말입니다.
대표적으로
Animated.Image, Animated.ScrollView, Animated.Text, Animated.View, Animated.FlatList, Animated.SectionList 가 있습니다.
하지만 이 외에도 다른 component 에 animation 을 추가하고 싶으면 어떻게 해야 할까요??
createAnimatedComponent() 함수를 사용하면 손쉽게 만들 수 있습니다.
밑에 예제는 styled-component 를 사용한 방법입니다.
// 첫번째 방법
const Box = styled(Animated.createAnimatedComponent(TouchableOpacity))`
width: 200px;
background-color: white;
`
// 두번째 방법
const Box = styled.TouchableOpacity`
width: 200px;
background-color: white;
`
const AnimatedBox = Animated.createAnimatedComponent(Box)
animation 의 state 는 절대 react 의 state 에 두지 않습니다.
value 가 필요하다면 AnmiatedAPI 에서 관리합니다.
Animated Value 는 절대 직접 관리하지 않습니다.
아무거나 animation 을 적용할 수 없습니다.
Animated.decay() <- 속도 제어 효과를 추가할 수 있습니다.
Animated.spring() <- bounce 효과를 추가할 수 있습니다.
Animated.timing() <- 일반적으로 많이 사용
const Box = styled.TouchableOpacity`
width: 200px;
height: 200px;
background-color: white;
`
const AnimatedBox = Animated.createAnimatedComponent(Box)
const App = () => {
const Y = new Animated.Value(0);
const moveUp = () => {
Animated.timing(Y, {
toValue: 200,
useNativeDriver: true
}).start()
}
return <AnimatedBox onPress={moveUp} style={{transform: [{translateY: Y}] }}/>
}
Animated.timing(value, options) 가 기본값입니다.
value 는 Animated 로 만든 value 값이 들어가 있고
options 는 animation 의 세부적인 기능을 만들 수 있습니다.
toValue 는 Y 값이 어느정도 움직일 수 있게 하는지 정해줍니다.
useNativeDriver 옵션은 애니메이션을 네이티브 쓰레드에서 실행할지 여부를 결정합니다.
이 옵션이 true로 설정되면 애니메이션은 네이티브 쓰레드에서 처리되므로 JavaScript 쓰레드와의 통신이 최소화됩니다.
이로 인해 성능이 향상되며, 부드럽고 빠른 애니메이션을 제공할 수 있습니다.
마지막으로 start() 까지 해주어야 애니메이션이 작동합니다.
하지만 Animated.createAnimatedComponent 로 만든 애니메이션은 실행해보면 가끔 애니메이션이 이상할 때가 있습니다. 그러한 이유는 TouchableOpacity 같은 경우 onPress 를 실행하면 살짝 배경이 투명해 지고 그다음에 Animated 가 작동됩니다. 그렇게 되면 두 애니메이션을 보게 되고 시각적으로 이상하게 느껴지는 것 입니다.
그럼 어떻게 바꾸어야 할까요?? 위에서 말한 Animated 의 기본값(예를 들어 Animated.View)에 내가 원하는 component 로 감싸주면 됩니다.
const Box = styled.View` <-- 변경
width: 200px;
background-color: white;
`
const AnimatedBox = Animated.createAnimatedComponent(Box)
const App = () => {
const Y = new Animated.Value(0);
const moveUp = () => {
Animated.timing(Y, {
toValue: 200,
useNativeDriver: true
}).start()
}
return (
<TouchableOpacity onPress={moveUp}>
<AnimatedBox style={{transform: [{translateY: Y}] }}/>
</TouchableOpacity>
)
}
그리고 만약 console.log(Y) 로 값을 확인하면 계속 0 이라고 뜨는 것을 볼 수 있습니다.
즉 animation 이 react component 를 다시 렌더링 하지 않는다는 뜻입니다. <- 아주 중요!!
spring 은 애니메이션에 바운스 효과를 주는 기능을 추가해 줍니다.
// ...생략
const App = () => {
const Y = new Animated.Value(0);
const moveUp = () => {
Animated.spring(Y, {
toValue: 200,
bounciness: 20, <-- 추가
useNativeDriver: true
}).start()
}
return (
<TouchableOpacity onPress={moveUp}>
<AnimatedBox style={{transform: [{translateY: Y}] }}/>
</TouchableOpacity>
)
}
이렇게 설정해주면 애니메이션 끝에 바운스 효과를 추가해줄 수 있습니다.
만약 두가지 애니메이션을 추가하고 싶다면 어떻게 해야 할까요??
예를 들어 클릭할 때 마다 위 아래로 왔다갔다 하는 애니메이션을 적용한다고 합시다.
이때 start 함수에 callback 함수를 넣을 수 있는데 이 callback 함수는
애니메이션이 끝난 후 실행이 됩니다.
이를 이용해서 두가지 애니메이션을 적용해 볼 수 있습니다.
여기서 중용한 부분은 useRef 를 사용해서 리렌더링을 통한 value 값 손실을 예방해야 합니다.
const App = () => {
const [up, setUp] = useState(false);
const Y = useRef(new Animated.Value(0)).current;
const toggleUp = () => setUp(prev => !prev)
const moveUp = () => {
Animated.timing(Y, {
toValue: up ? 200 : -200,
useNativeDriver: true,
easing: Easing.elastic(3)
}).start(toggleUp)
}
return (
<TouchableOpacity onPress={moveUp}>
<AnimatedBox style={{transform: [{translateY: Y}] }}/>
</TouchableOpacity>
)
}
왜 useRef 로 감싸주어야 할까요?
useState 값이 바뀌게 되면 react 는 해당 컴포넌트를 리렌더링 합니다.
그 과정에서 new Animated.Value(0) 값이 다시 0 으로 초기화 되기 때문에 애니메이션이 정상적으로 작동하지 않을 수 있습니다.
React Native의 Animated API에서 사용되며, 애니메이션의 입력 범위를 다른 범위로 변환하는 데 사용됩니다.
만약 Y 의 값이 300 ~ -300 으로 이동하고 있을때
opacity 값이 1 ~ 0 ~ 1 로 가고싶다면 interpolate 를 사용하면 됩니다.
<-----------------------------------------> 이런식으로 이동할 때
1 0.9 0.8 0.7 0.6 ---------- 0.7 0.8 0.9 1 의 opacity 값을 가지는 경우입니다.
const App = () => {
const [up, setUp] = useState(false);
const Y = useRef(new Animated.Value(300)).current;
const toggleUp = () => setUp(prev => !prev)
const moveUp = () => {
Animated.timing(Y, {
toValue: up ? 200 : -200,
useNativeDriver: true,
duration: 5000
}).start(toggleUp)
}
const opacityValue = Y.interpolate({ <---- interpolate
inputRange: [-300, 0, 300],
outputRange: [1, 0, 1]
})
return (
<TouchableOpacity onPress={moveUp}>
<AnimatedBox style={{
opacity: opacityValue, <---- 넣기
transform: [{translateY: Y}]
}}/>
</TouchableOpacity>
)
}
변경된 값을 보고 싶다면 이렇게 해주어야 합니다.
Y.addListner(() => console.log(opacityValue)
또한 interpolate 는 무궁무진하게 이용할 수 있습니다.
예를 들어 ["360deg", "-360deg"] 처럼 문자열로도 이용할 수 있습니다.
["360deg", "-360deg"]
["rgb(255,153,11), "rgb(255, 240, 33)"]<- useNativeDriver: false 로..
만약 적용이 안되는 interpolate 옵션이 있다면 useNativeDriver 에서 에러를 발생하기 때문에 false 로 바꾸어 주면 사용할 수 있습니다.
위에서 사용했던 new Animated.Value() 는 하나의 값만 가집니다.
여러개의 값을 가지고 싶다면 어떻게 해야 할까요??
new Animated.ValueXY({x: 0, y: 0}) 라고 사용하면 됩니다.
이렇게 만들어 두면 x, y 를 interpolate 를 이용해서 손쉽게 사용할 수 있습니다.
const App = () => {
const [up, setUp] = useState(false);
const Y = useRef(new Animated.ValueXY({x: 0, y: 0})).current;
const toggleUp = () => setUp(prev => !prev)
const moveUp = () => {
Animated.timing(Y, {
toValue: up ? 200 : -200,
useNativeDriver: true,
duration: 5000
}).start(toggleUp)
}
const opacityValue = Y.y.interpolate({ <-- y 꺼내주기
inputRange: [-300, 0, 300],
outputRange: [1, 0, 1]
})
return (
<TouchableOpacity onPress={moveUp}>
<AnimatedBox style={{
opacity: opacityValue,
transform: [{translateY: Y.y}] <---- y 꺼내기
}}/>
</TouchableOpacity>
)
}
// styled-component 생략
const App = () => {
const POSITION = useRef(new Animated.ValueXY({x: 0, y: 0})).current
const topLeft = Animated.timing(POSITION, {
toValue: {
x: -100,
y: 100
},
useNativeDriver: true
})
const bottomLeft = Animated.timing(POSITION, {
toValue: {
x: -100,
y: -100,
},
useNativeDriver: true
})
const moveUp = () => {
Animated.loop( <-- 중요
Animated.sequence([topLeft, bottomLeft]) <-- 중요
)
}
return (
<TouchableOpacity onPress={moveUp}>
<AnimatedBox style={{
opacity: opacityValue,
transform: [...Animated.getTranslateTransfrom()] <-- 중요
// Animated.getTranslateTransfrom() 그냥 이것만 넣어줘도 됨.
}}/>
</TouchableOpacity>
)
}
코드를 보면 새로운 모습이 보일 것입니다.
Animated.loop 는 애니메이션을 무한 반복할지 정해줍니다.
Animated.sequence 는 Animated.timing 으로 만든 애니메이션을 배열 형태인 sequence 에 넣어서 차례대로 실행시켜 줍니다.
Animated.getTranslateTransfrom() 는 개발자들의 귀찮음으로 인해 만들어진 함수로써
[{translateX: POSITION.x}, {translateY: POSITION.y}] 처럼 만들어 줍니다.
기본적으로 손가락의 제스처나 움직임을 감지할 수 있게 해줍니다.
import { Animated, PanResponder } from 'react-native';
const panResponder = React.useRef(PanResponder.create({})).current
이렇게 만든 panResponder 을 console.log 로 확인해보면

이 사진에서 중요하게 볼 부분은 panHandlers 입니다.
panHandlers 안에 있는 함수들을 손가락 애니메이션에 적용할 View 에 넣어 주어야 합니다. 즉 사용자가 drag 할 때 그 view 가 반응하기를 원한다면 말이죠.
<TouchableOpacity onPress={moveUp}>
<AnimatedBox
{...panResponder.panHandlers} <------
style={{
opacity: opacityValue,
transform: [...POSITION.getTranslateTransform()]
}}
/>
</TouchableOpacity>
이 함수는 PanResponder.create({}) 에서 객체 안에 사용하는 함수인데
이 함수가 true 를 반환하면 PanResponder 가 여러분의 사각형에서 터치를 감지하게 됩니다.
이 함수는 마찬가지로 PanResponder.create({}) 에서 객체 안에 사용하는 함수인데
거리가 바뀌면 호출되는 함수 입니다.
const panResponder = React.useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (_, gestureState) => {
console.log(gestureState)
}
})).current
gestureState 를 console.log 로 확인을 하면 다양한 값이 나옵니다.
그중에서 dx, dy 값이 있는데 내가 손으로 어디까지 도형을 누르고 어디까지 움직였는지
확인할 수 있는 값입니다.
이걸 이용해서 드래그 기능을 만들 수 있습니다.
const panResponder = React.useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (_, {dx, dy}) => {
POSITION.setValue({
x: dx,
y: dy
})
}
})).current
setValue 는 기존에 만들어 뒀던 ValueXY 에 값을 변경해주는 함수입니다.
이렇게 코드를 짜면 손가락으로 도형을 여기저기 이동시킬 수 있습니다.
사용자가 해당 도형에 손가락을 떼면 동작하는 함수 입니다.
이렇게 하면 좀 더 부드러운 애니메이션을 볼 수 있습니다.
const panResponder = React.useRef(PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (_, {dx, dy}) => {
POSITION.setValue({
x: dx,
y: dy
})
},
onPanResponderRelease: () => {
Animated.spring(POSITION, {
toValue: {
x: 0,
y: 0
},
useNativeDriver: false
}).start();
}
})).current