elevne's Study Note

React Native (4-4: RN 컴포넌트 알아보기(PanResponder, Animated)) 본문

Frontend/React Native

React Native (4-4: RN 컴포넌트 알아보기(PanResponder, Animated))

elevne 2023. 2. 22. 23:28

PanResponserTouchableHighlight, Pressable, Button 등과 다르게 컴포넌트가 아니라 React Native 에서 제공하는 하나의 API로 여러 터치를 하나의 제스처로 조정해준다. 이는 Extra Touch 들에 대해 탄력적인 Single-Toch 를 만들어주고, 기본적인 Multi-Touch Gesture 을 인식하는데 사용될 수 있다고 한다. PanResponder gestureState 객체를 사용하여 현재 제스처의 속도, 누적 이동거리 등과 같은 원시 위치 데이터에 접근할 수 있다. 아래와 같은 형식으로 PanResponder 의 함수를 사용할 수 있다.

 

 

 

onPanResponderMove: (event, gestureState) => {}

 

 

 

위 코드에서 사용되는 eventnative event object 로, PressEvent 형태의 합성 터치 이벤트라고 한다. 또, gestureState 는 아래와 같은 것들을 가진다.

 

 

 

stateID Gesture 상태의 ID
moveX 마지막으로 이동한 터치의 화면 좌표 (x)
moveY 마지막으로 이동한 터치의 화면 좌표 (y)
x0 응답자 부여의 화면 좌표 (x)
y0 응답자 부여의 화면 좌표 (y)
dx 터치가 시작된 이후 제스처의 누적거리 (x) ( dx = x0 + dx )
dy 터치가 시작된 이후 제스처의 누적거리 (y) ( dy = y0 + dy )
vx 현재 속도 (x)
vy 현재 속도 (y)
numberActiveTouches 현재 화면의 터치 수

 

 

 

PanResponder 은 복잡한 제스처를 처리하기 위해 Animated API 와 같이 사용된다. 아래와 같이 코드를 작성해볼 수 있다.

 

 

 

import React, {useRef} from 'react';
import {Animated, View, StyleSheet, PanResponder, Text} from 'react-native';

const App = () => {
  const pan = useRef(new Animated.ValueXY()).current;

  const panResponder = useRef(
    PanResponder.create({
      onMoveShouldSetPanResponder: () => true,
      onPanResponderMove: Animated.event([null, {dx: pan.x, dy: pan.y}]),
      onPanResponderRelease: () => {
        pan.extractOffset();
      },
    }),
  ).current;

  return (
    <View style={styles.container}>
      <Text style={styles.titleText}>Drag this box!</Text>
      <Animated.View
        style={{
          transform: [{translateX: pan.x}, {translateY: pan.y}],
        }}
        {...panResponder.panHandlers}>
        <View style={styles.box} />
      </Animated.View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  titleText: {
    fontSize: 14,
    lineHeight: 24,
    fontWeight: 'bold',
  },
  box: {
    height: 150,
    width: 150,
    backgroundColor: 'blue',
    borderRadius: 5,
  },
});

export default App;

 

 

result

 

 

 

위와 같이 컴포넌트 내에서 create 메서드, 그 안에 사용할 함수들을 작성하여 사용한다. onStartShouldSetPanResponder, onMoveShouldSetPanResponder 함수는 주어진 터치 이벤트에 반응할지를 결정한다. onPanResponderGrant 는 터치 이벤트가 발생할 때 실행, onPanResponderRelease, onPanResponderTerminate 는 터치이벤트가 끝날 때 실행된다. 또, 터치 이벤트가 진행 중일 때는 onPanResponderMove 를 통해 터치 정보를 확인할 수 있다.

 

 

 

또, 위에서 PanResponderAnimated 와 함께 사용된다고 했다. Animated 라이브러리는 애니메이션을 유연하고 강력하며 제작 및 유지관리에 어려움이 없도록 설계되었다. Animated 는 입력과 출력 사이의 Declarative Relation, 그 사이의 구성 가능한 변환, 시간 기반 애니메이션 실행을 제어하는 시작/정지 방법에 초점을 맞춘다고 한다. 그래서, 애니메이션을 구현하기 위한 핵심은 Animated.Value 를 만들고 Style attribute 나 animated component 에 연결하여  Animated.timing() 을 사용하여 업데이트를 수행하는 것이다. 아래는 Animated 를 사용한 코드 예시이다.

 

 

 

import React, {useRef} from 'react';
import {
  Animated,
  Text,
  View,
  StyleSheet,
  Button,
  SafeAreaView,
} from 'react-native';

const App = () => {
  // fadeAnim will be used as the value for opacity. Initial Value: 0
  const fadeAnim = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    // Will change fadeAnim value to 1 in 5 seconds
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 5000,
      useNativeDriver: true,
    }).start();
  };

  const fadeOut = () => {
    // Will change fadeAnim value to 0 in 3 seconds
    Animated.timing(fadeAnim, {
      toValue: 0,
      duration: 3000,
      useNativeDriver: true,
    }).start();
  };

  return (
    <SafeAreaView style={styles.container}>
      <Animated.View
        style={[
          styles.fadingContainer,
          {
            // Bind opacity to animated value
            opacity: fadeAnim,
          },
        ]}>
        <Text style={styles.fadingText}>Fading View!</Text>
      </Animated.View>
      <View style={styles.buttonRow}>
        <Button title="Fade In View" onPress={fadeIn} />
        <Button title="Fade Out View" onPress={fadeOut} />
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  fadingContainer: {
    padding: 20,
    backgroundColor: 'powderblue',
  },
  fadingText: {
    fontSize: 28,
  },
  buttonRow: {
    flexBasis: 100,
    justifyContent: 'space-evenly',
    marginVertical: 16,
  },
});

export default App;

 

 

result

 

 

 

 

ReactNative 에서는 애니메이션에 사용할 수 있는 4 가지의 컴포넌트 (Animated.Image, Animated.ScrollView, Animated.Text, Animated.View) 를 제공한다. 또, 세 가지의 애니메이션 타입이 있다. Timing 은 시간이 지남에 따라 값을 Animate 하는 것, Spring 은 간단한 스프링 물리 모델을 제공하는 것, 그리고 Decay 는 초기 속도로 시작하여 점차 정지하는 것이다. Scale, Opacity, toValue, Duration 등 다양한 속성을 사용할 수 있다.

 

 

 

마지막으로, 위에서 계속 사용된 useRef 에 대해서 알아보았다. useRef.current 속성이 전달된 인수(InitialValue)로 초기화된 가변 참조 개체를 반환한다. 반환된 개체는 구성 요소의 전체 수명 동안 유지된다. useRef 는 .current 속성에서 변경 가능한 값을 보유할 수 있는 상자와 같다고 한다. <div ref={myRef} /> 와 같이 작성하여 ref 객체를 전달하면 해당 노드가 변경될 때마다 React 의 .current 속성이 해당 DOM 노드로 설정되는 것이다. 아래는 useRef 를 활용한 예시 코드이다.

 

 

 

import React, { useState, useRef } from "react";

function ManualCounter() {
  const [count, setCount] = useState(0);
  const intervalId = useRef(null);
  console.log(`랜더링... count: ${count}`);

  const startCounter = () => {
    intervalId.current = setInterval(
      () => setCount((count) => count + 1),
      1000
    );
    console.log(`시작... intervalId: ${intervalId.current}`);
  };

  const stopCounter = () => {
    clearInterval(intervalId.current);
    console.log(`정지... intervalId: ${intervalId.current}`);
  };

  return (
    <>
      <p>자동 카운트: {count}</p>
      <button onClick={startCounter}>시작</button>
      <button onClick={stopCounter}>정지</button>
    </>
  );
}

 

 

 

 

 

 

 

Reference:

https://www.daleseo.com/react-hooks-use-ref/

https://velog.io/@he0_077/React-Native-PanResponder

https://www.devh.kr/2020/Introduction-to-animation-in-React-Native/

빠른 모바일 앱 개발을 위한 React Native