티스토리 뷰

react native

React Native 에서 Animated 사용하기

YG - 96년생 , 강아지 있음, 개발자 희망 2022. 2. 5. 08:41

사용방법

import { Animated } from "react-native";

 

Animated를 사용할 때 중요한 점

1.Animated의 state를 React useState 값으로 두지 않는다.

  const Y = new Animated.Value(0);

value 가 필요하다면 Animated의 API에서 불러온다.

 

2.Animated의 Value 는 절대 직접 수정하지 않는다.

오로지 위의 3가지 방법으로 수정할 수 있다.

3. 아무 컴포넌트나 Animate 할 수 없다.

 

const AnimatedBox2 = Animated.createAnimatedComponent(TouchableOpacity);

이렇게 Animated.createAnimatedComponent를 통해 생성된 컴포넌트만 사용 가능하다.

 

위의 방법처럼 사용하면 TouchableOpacity가 import 되는데 이렇게 하지 않고 다르게 이용하는 방법은

 

const Box = styled.View`
  width: 200px;
  height: 200px;
  background-color: tomato;
`;

const AnimatedBox = Animated.createAnimatedComponent(Box);

이렇게 먼저 styled로 만든 후 Animated로 감싸주는 방법으로 할 수 있다.

 

 

예시를 통해서 정리하려고 합니다.

 

  const Y = new Animated.Value(0);
  const moveUp = () => {
    Animated.timing(Y, {
      toValue: 200,
      useNativeDriver: true,
    }).start();
  }; 
  
  return (
    <Container>
      <AnimatedBox
        onPress={moveUp}
        style={{ transform: [{ translateY: Y }] }}
      />
    </Container>
  );

 

Animated 함수를 통해 Value를 수정하는 방법

 

먼저 moveUp이라는 함수를 만들었습니다.

 

중요한 점만 설명해드리자면 moveUp에 보시면 Animated.timing(값, {

toValue: 가고자 하는 값,

useNativeDriver: ( 휴대폰 네이티브 방식(안드로이드, ios)으로 애니메이션을 움직일 것인지) true 

//

  • useNativeDriver: Uses the native driver when true. Default false

//

}

두 가지는 꼭 입력해주시는 것이 좋습니다.

 

그리고 AnimatedBox는 TouchableOpacity로 onPress와 style에 함수와 변수를 입력해줍니다.

 

decay

timing

spring

 

위의 3가지 함수와 함수 안의 옵션들로 다양한 애니메이션을 줄 수 있습니다.

 

 

decay, timing , spring 애니메이션이 잘 작동하는 모습입니다.

 

 

Y의 값을 보고 싶을 땐 이렇게 확인하시면 됩니다.

 

 Y.addListener(() => console.log(Y._value));//웹에서 확인
  Y.addListener(() => console.log(Y)); // 모바일에서 이용할때 터미널에서 확인

Y의 값이 나오는 모습

 

 

 

useRef의 중요성

이제 클릭하면 상자를 위와 아래로 움직이도록 해보겠습니다.

export default function App() {
  const [up, setUp] = useState(false);
  const Y = new Animated.Value(0);
  const toggle = () => {
    setUp((prev) => !prev);
  };
  const moveUp = () => {
    Animated.timing(Y, {
      toValue: up ? 200 : -200,
      useNativeDriver: true,
    }).start(toggle);
  };

  Y.addListener(() => console.log(Y._value)); //웹에서 확인
  return (
    <Container>
      <AnimatedBox
        onPress={moveUp}
        style={{ transform: [{ translateY: Y }] }}
      />
    </Container>
  );
}

 

 

 

이상한 점은 위의 코드를 실행하였을 때 위아래로만 가는 것이 아니라 가운데로 초기화된다는 것입니다.

console.log를 찍어보면

  Y.addListener(() => console.log("실시간 변화값", Y._value)); //웹에서 확인
  console.log(Y, "의 값으로 렌더되었습니다");

0으로 초기화 되는 것을 알 수 있습니다.

그 이유는 up의 상태가 변하였기에 새롭게 렌더링을 하는 과정에서 0으로 값을 초기화하는 것인데 이를 고치려면 useRef를 이용하면 됩니다.

 

  const Y = useRef(new Animated.Value(0)).current;

 

 

 

useRef를 사용하니 0으로 새로 렌더링을 하지 않게 되었습니다.

 

-200~200으로 이동하는 모습

 

 

Interpolation에

 

어느 인풋 값을 주어주면 인풋 값을 따라서 아웃풋을 제공해주는 기능입니다.

 

export default function App() {
  const [up, setUp] = useState(false);
  const Y_POSITION = useRef(new Animated.Value(0)).current;
  const toggle = () => {
    setUp((prev) => !prev);
  };
  const borderRadius = Y_POSITION.interpolate({
    inputRange: [-300, 300],
    outputRange: [0, 100],
  });

  const opacity = Y_POSITION.interpolate({
    inputRange: [-300, -100, 100, 300],
    outputRange: [1, 0.1, 0.1, 1],
  });
  const moveUp = () => {
    Animated.timing(Y_POSITION, {
      toValue: up ? 300 : -300,
      useNativeDriver: true,
    }).start(toggle);
  };

  Y_POSITION.addListener(() => console.log("실시간 변화값", Y_POSITION._value)); //웹에서 확인
  // console.log(Y_POSITION._value, "의 값으로 렌더되었습니다");
  return (
    <Container>
      <AnimatedBox
        onPress={moveUp}
        style={{
          borderRadius,
          opacity,
          transform: [{ translateY: Y_POSITION }],
        }}
      />
    </Container>
  );
}

const 이름 = 참고할 변수. interpolate({

inputRange:[참고할 값],

outputRange:[얻고자 하는 값]

}} 

이렇게 이용하시면 되시고 인풋과 아웃풋의 배열 안에 개수는 동일해야 합니다.

 

 

 

interpolate를 이용한 이미지입니다

 

또한 interpolate를 이용해서 string도 아웃풋으로 받을 수 있습니다. 대신 인풋은 숫자만 넣을 수 있습니다.

 

예시로 각도와 배경색을 바꿔보겠습니다

 

export default function App() {
  const [up, setUp] = useState(false);
  const Y_POSITION = useRef(new Animated.Value(0)).current;
  const toggle = () => {
    setUp((prev) => !prev);
  };
  const borderRadius = Y_POSITION.interpolate({
    inputRange: [-300, 300],
    outputRange: [0, 100],
  });

  const rotateY = Y_POSITION.interpolate({
    inputRange: [-300, 300],
    outputRange: ["-360deg", "360deg"],
  });
  const bgColor = Y_POSITION.interpolate({
    inputRange: [-300, 300],
    outputRange: ["rgb(64, 121, 137)", "rgb(254, 100, 155)"],
  });
  const moveUp = () => {
    Animated.timing(Y_POSITION, {
      toValue: up ? 300 : -300,
      useNativeDriver: true,
    }).start(toggle);
  };

  Y_POSITION.addListener(() => console.log("실시간 변화값", Y_POSITION._value)); //웹에서 확인
  // console.log(Y_POSITION._value, "의 값으로 렌더되었습니다");
  return (
    <Container>
      <AnimatedBox
        onPress={moveUp}
        style={{
          borderRadius,
          backgroundColor: bgColor,
          transform: [{ translateY: Y_POSITION }, { rotateY }],
        }}
      />
    </Container>
  );
}

 

 

 

rgb와 deg 가 변하는 모습입니다.

 

 

web에서는 오류가 나지 않지만 휴대폰에서는 오류가 날 것입니다.

 

  const moveUp = () => {
    Animated.timing(Y_POSITION, {
      toValue: up ? 300 : -300,
      useNativeDriver: false,
    }).start(toggle);
  };

rgb를 변화시키는 것은 useNativeDriver을 false 해주어서 javascript로 렌더 시켜야 하기 때문입니다

 

그래서 가끔씩 native에서 interpolate를 못하는 경우에 useNativeDriver을 멈춰야 하는 경우가 있는 데 어떤 것을 interpolate를 할지 주의 깊게 생각해서 할 것입니다.

 

 

ValueXY

x, y값을 같이 value를 줄 수 있는 기능입니다.

 

  const POSITION = useRef(
    new Animated.ValueXY({
      x: -SCREEN_WIDTH / 2 + 100,
      y: -SCREEN_HEIGHT / 2 + 100,
    })
  ).current;

이와 같이 Value에서 ValueXY({

x:값,

y:값

})으로 사용하면 됩니다.

 

또한 style에 보면 transform:[{translateY}, {translateX}]처럼 이용하고 있는데 간단하게 이용 가능한 함수를 제공하고 있습니다.

 

      <AnimatedBox
        {...panResponder.panHandlers}
        style={{
          borderRadius,
          backgroundColor: bgColor,
          transform: [{ translateY: POSITION.y }, { translateX: POSITION.x }],
        }}
      />

 

getTranslateTransform()

 

{x, y}을(를) 사용 가능한 변환으로 변환합니다(예:

style={{
  transform: this.state.anim.getTranslateTransform()
}}

 

혹은

 

      <AnimatedBox
        {...panResponder.panHandlers}
        style={{
          borderRadius,
          backgroundColor: bgColor,
          transform: [...POSITION.getTranslateTransform()],
        }}
      />

이 코드를 사용하면 위의 css를 적용한 것으로 표시됩니다.

 

 

loop와 sequence

지정된 애니메이션을 연속적으로 루프 하여 끝에 도달할 때마다 재설정하고 처음부터 다시 시작합니다.

 

loop( 애니메이션 값, 설정)으로 사용할 수 있습니다.

 

애니메이션을 순서대로 시작하고 각 애니메이션이 완료될 때까지 기다린 후 다음 애니메이션을 시작합니다. 현재 실행 중인 애니메이션이 중지되면 다음 애니메이션이 시작되지 않습니다.

 

sequence(애니메이션 값의 배열)으로 사용 가능합니다.

 

각 모서리를 도는 예시

import React, { useRef, useState } from "react";
import { Animated, Dimensions } from "react-native";
import styled from "styled-components/native";

const Container = styled.View`
  flex: 1;
  justify-content: center;
  align-items: center;
`;

const Box = styled.TouchableOpacity`
  width: 200px;
  height: 200px;
  background-color: tomato;
`;

const AnimatedBox = Animated.createAnimatedComponent(Box);

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");

export default function App() {
  const POSITION = useRef(
    new Animated.ValueXY({
      x: -SCREEN_WIDTH / 2 + 100,
      y: -SCREEN_HEIGHT / 2 + 100,
    })
  ).current;

  const borderRadius = POSITION.y.interpolate({
    inputRange: [-300, 300],
    outputRange: [0, 100],
  });

  const bgColor = POSITION.y.interpolate({
    inputRange: [-300, 300],
    outputRange: ["rgb(64, 121, 137)", "rgb(254, 100, 155)"],
  });

  const bottomRight = Animated.timing(POSITION, {
    toValue: {
      x: SCREEN_WIDTH / 2 - 100,
      y: SCREEN_HEIGHT / 2 - 100,
    },
    useNativeDriver: false,
  });

  const bottomLeft = Animated.timing(POSITION, {
    toValue: {
      x: -SCREEN_WIDTH / 2 + 100,
      y: SCREEN_HEIGHT / 2 - 100,
    },
    useNativeDriver: false,
  });

  const topLeft = Animated.timing(POSITION, {
    toValue: {
      x: -SCREEN_WIDTH / 2 + 100,
      y: -SCREEN_HEIGHT / 2 + 100,
    },
    useNativeDriver: false,
  });

  const topRight = Animated.timing(POSITION, {
    toValue: {
      x: SCREEN_WIDTH / 2 - 100,
      y: -SCREEN_HEIGHT / 2 + 100,
    },
    useNativeDriver: false,
  });

  const moveUp = () => {
    Animated.loop(
      Animated.sequence([bottomLeft, bottomRight, topRight, topLeft])
    ).start();
  };

  POSITION.addListener(() => console.log("실시간 변화값", POSITION.x._value)); //웹에서 확인
  POSITION.addListener(() => console.log("실시간 변화값", POSITION.y._value)); //웹에서 확인
  // console.log(POSITION._value, "의 값으로 렌더되었습니다");
  return (
    <Container>
      <AnimatedBox
        onPress={moveUp}
        style={{
          borderRadius,
          backgroundColor: bgColor,
          transform: [{ translateY: POSITION.y }, { translateX: POSITION.x }],
        }}
      />
    </Container>
  );
}

 

 

 

이렇게 각 모서리를 loop와 sequence, valueXY 기능을 이용해서 애니메이션 돌도록 해보았습니다

 

 

이제 사용자의 터치로 애니메이션 되는 것을 이용해보려고 합니다

panResponder

PanResponder는 여러 번의 터치를 하나의 제스처로 조정합니다. 단일 터치 제스처를 추가 터치에 탄력적으로 만들고 기본 멀티 터치 제스처를 인식하는 데 사용할 수 있습니다.

 

공식문서에서 보면 useRef를 사용하라고 나와있습니다.

공식 문서

 

  const panResponder = useRef(PanResponder.create({})).current;

  console.log(panResponder);

 

이벤트를 콘솔로그 해보았을때

이렇게 많은 함수들이 사용자의 터치를 인식하고 도와주는 함수들인데 이러한 함수들을 이용할 View Props 형태로 넘겨주면 panResponder을 이용할 수 있게 됩니다.

const Box = styled.View` // View 이여야만 합니다.
  width: 200px;
  height: 200px;
  background-color: tomato;
`;

const AnimatedBox = Animated.createAnimatedComponent(Box);

return (
    <Container>
      <AnimatedBox
      {...panResponder.panHandlers}
        style={{
          borderRadius,
          backgroundColor: bgColor,
          transform: [{ translateY: POSITION.y }, { translateX: POSITION.x }],
        }}
      />
    </Container>
  );
}

 

panResponder의 원하는 함수를 이용하는 방법

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderMove: (evt, gestureState) => {
        console.log(evt, gestureState);
      },
    })
  ).current;

onStartShouldSetPanResponder: (evt, gestureState) => true, 이것은 터치를 인식하기 시작할 것인지에 대한 함수입니다.

 

사용자가 원을 누를 때 우리는 활성화되어야 할까요?
 

페이스북에서 함수에 대해 설명을  정리한 깃입니다.

 

 

 

onPanResponderMove: (evt, gestureState) => {
// The most recent move distance is gestureState.move {X, Y}
// The accumulated gesture distance since becoming responder is
// gestureState.d {x, y}
}, 이것은 사용자가 터치를 하고 어디로 위치하는지 x, y 값을 주어주는 함수입니다.

 

이벤트를 콘솔로그 해보았을 때 x,y 값이 나옵니다.

 

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderMove: (evt, { dx, dy }) => {
        console.log(dx, dy);
      },
    })
  ).current;

x,y값이 나오는 모습

 

 

setValue()

값을 직접 설정합니다. 그러면 값에서 실행 중인 애니메이션이 중지되고 바인딩된 속성이 모두 업데이트됩니다.

 

맨 처음에 중요한 점으로 직접 값을 수정하면 안 된다고 적었었는데 setValue를 통해 애니메이션 동작 없이 직접 값을 수정할 수 있도록 도와주는 함수가 있습니다.

 

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderMove: (evt, { dx, dy }) => {
        POSITION.setValue({
          x: dx,
          y: dy,
        });
      },
    })
  ).current;

  //console.log(panResponder);

  POSITION.addListener(() => console.log("실시간 변화값", POSITION.x._value)); //웹에서 확인
  POSITION.addListener(() => console.log("실시간 변화값", POSITION.y._value)); //웹에서 확인
  // console.log(POSITION._value, "의 값으로 렌더되었습니다");
  return (
    <Container>
      <AnimatedBox
        {...panResponder.panHandlers}
        style={{
          borderRadius,
          backgroundColor: bgColor,
          transform: [...POSITION.getTranslateTransform()],
        }}
      />
    </Container>
  );
}

 

수정할 변수. setValue({

x:수정할 값,

y:수정할 값

)} 이렇게 이용하시면 됩니다.

 

 

 

 

이렇게 setValue와 panResponder을 사용하게 되면  사용자의 터치를 인식하여 움직이게 되고 interpolate를 이용한 애니메이션 값도 정상적으로 작동이 됩니다

 

onPanResponderRelease()

onPanResponderRelease:(evt, gestureState)=>{
// The user has released all touches while this view is the
// responder. This typically means a gesture has succeeded
}, 사용자가 터치를 그만두고 손가락을 뺏을 때 함수를 호출할 수 있는 함수입니다.

 

이 보기 동안 사용자가 모든 터치를 해제했습니다. // 응답자 이는 일반적으로 제스처가 성공했음을 의미합니다.

 

  const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderMove: (evt, { dx, dy }) => {
        POSITION.setValue({
          x: dx,
          y: dy,
        });
      },
      onPanResponderRelease: () => {
        Animated.spring(POSITION, {
          toValue: {
            x: 0,
            y: 0,
          },
          bounciness: 20,
          useNativeDriver: false,
        }).start();
      },
    })
  ).current;

사용자가 터치를 땟을 때 원점으로 돌아가게 해 보았습니다.

 

 

 

원점으로 애니메이션이 작동하면서 돌아가는 모습입니다. 만약 POSITION.setValue({

x:0,

y:0})으로 원점으로 돌아가게 한다면 애니메이션이 작동되지 않고 순간 이동하는 모습을 볼 수 있을 것입니다.

 

 

 

offset

 

setOffset()

setValue, 애니메이션 또는 Animated.Event를 통해 설정된 값 위에 적용되는 오프셋을 설정합니다. 팬 제스처의 시작과 같은 것들을 보상하는 데 유용합니다.

 

flattenOffset()

 

오프셋 값을 기준 값으로 병합하고 오프셋을 0으로 재설정합니다. 값의 최종 출력은 변경되지 않습니다.

 

extractOffset()

오프셋 값을 기준 값으로 설정하고 기준 값을 0으로 재설정합니다. 값의 최종 출력은 변경되지 않습니다.
 
 const panResponder = useRef(
    PanResponder.create({
      onStartShouldSetPanResponder: () => true,
      onPanResponderGrant: () => {
        console.log("터치 시작");
        POSITION.setOffset({
          x: POSITION.x._value,
          y: POSITION.y._value,
        });
      },
      onPanResponderMove: (evt, { dx, dy }) => {
        console.log("터치 중");
        POSITION.setValue({
          x: dx,
          y: dy,
        });
      },
      onPanResponderRelease: () => {
        console.log("터치 끝");
        POSITION.flattenOffset();
      },
    })
  ).current;

 

onPanResponderMove에서 dx, dy 가 손가락이 얼마나 움직였는지의 거리였다면

다시 터치하였을 때 dx, dy값이 0으로 초기화되기 때문에 두 번째 터치부터는 다시 원점으로 돌아가는 증상이 있었다.

 

따라서 onPanResponderGrant : 터치가 시작할 때 맨 처음 동작하는 함수에서 setOffset을 이용하여 x, y값에 position.x, y의 값을 입력해주어서 시작 위치가 변하지 않게 했고 

 

 onPanResponderRelease : 터치가 끝날 때 작동하는 함수에서 flattenOffset() 함수를 이용하여 이동한 거리의 offset 값인 dx, dy값을 초기화 시켜주었다. 따라서 터치 시작할 때마다 중첩되는 dx,dy 값의 오류가 없어진 것이다.

 

 

 

콘솔로그창

 

 

 


React Native의 Animated에 대해 공부해보았는데 재밌는 기능도 많고 사용하기 좋은 옵션들도 많아서 잘 쓰면 예쁘게 어플을 만드는데 도움이 될 것 같습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함