리액트 네이티브 - 5

박찬영·2024년 4월 12일
post-thumbnail

animation

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)

결론

  1. animation 의 state 는 절대 react 의 state 에 두지 않습니다.
    value 가 필요하다면 AnmiatedAPI 에서 관리합니다.

  2. Animated Value 는 절대 직접 관리하지 않습니다.

  3. 아무거나 animation 을 적용할 수 없습니다.


animated 사용

Animated.decay() <- 속도 제어 효과를 추가할 수 있습니다.
Animated.spring() <- bounce 효과를 추가할 수 있습니다.
Animated.timing() <- 일반적으로 많이 사용

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 를 다시 렌더링 하지 않는다는 뜻입니다. <- 아주 중요!!

Animated.spring()

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 으로 초기화 되기 때문에 애니메이션이 정상적으로 작동하지 않을 수 있습니다.

interpolate

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 로 바꾸어 주면 사용할 수 있습니다.

ValueXY

위에서 사용했던 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.sequenceAnimated.timing 으로 만든 애니메이션을 배열 형태인 sequence 에 넣어서 차례대로 실행시켜 줍니다.
Animated.getTranslateTransfrom() 는 개발자들의 귀찮음으로 인해 만들어진 함수로써
[{translateX: POSITION.x}, {translateY: POSITION.y}] 처럼 만들어 줍니다.


손가락 애니메이션

PanResponder

기본적으로 손가락의 제스처나 움직임을 감지할 수 있게 해줍니다.

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>

onStartShouldSetPanResponder

이 함수는 PanResponder.create({}) 에서 객체 안에 사용하는 함수인데
이 함수가 true 를 반환하면 PanResponder 가 여러분의 사각형에서 터치를 감지하게 됩니다.

onPanResponderMove

이 함수는 마찬가지로 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 에 값을 변경해주는 함수입니다.
이렇게 코드를 짜면 손가락으로 도형을 여기저기 이동시킬 수 있습니다.

onPanResponderRelease

사용자가 해당 도형에 손가락을 떼면 동작하는 함수 입니다.
이렇게 하면 좀 더 부드러운 애니메이션을 볼 수 있습니다.


  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
profile
오류는 도전, 코드는 예술

0개의 댓글