어플리케이션을 쓰다보면 스크롤에 따라 헤더나 텍스트의 위치 및 크기가 바뀌는 UI를 제공하는 경우가 종종 있습니다.
이런 UI는 제한된 모바일 화면의 크기 안에서 비교적 많은 양의 정보를 보여줌과 동시에 좀 더 사용자와 상호작용을 하고있다는 느낌을 줍니다.
React Native에는 Animated API 가 있습니다. Animated
를 사용하여 쉽게 컴포넌트에 애니메이션 효과를 줄 수 있습니다.
import React, { useRef } from "react";
import { Animated, Text, View, StyleSheet, Button, SafeAreaView } from "react-native";
const App = () => {
// fadeAnim: 컴포넌트의 opacity 속성에 적용할 것입니다. 초기값은 0입니다.
const fadeAnim = useRef(new Animated.Value(0)).current;
const fadeIn = () => {
// fadeAnim 값이 1이 되도록 5초동안 변화시킵니다.
Animated.timing(fadeAnim, {
toValue: 1,
duration: 5000
}).start();
};
const fadeOut = () => {
// fadeAnim 값이 0이 되도록 3초동안 변화시킵니다.
Animated.timing(fadeAnim, {
toValue: 0,
duration: 3000
}).start();
};
return (
<SafeAreaView style={styles.container}>
<Animated.View
style={[
styles.fadingContainer,
{
// fadeAnim 값을 opacity에 적용합니다.
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;
간단한 예제를 확인했으니 이제는 실제로 스크롤에 따라 컴포넌트를 동적으로 변화시키는 방법을 구현해보겠습니다. 여기에서는 헤더 컴포넌트를 예로 들어서 설명합니다.
ScrollView
에 Animated
를 적용하기 위해서는 RN에서 제공되는 기본 ScrollView
컴포넌트 대신에 Animated.ScrollView
컴포넌트를 사용해야 합니다.
스크롤의 위치는 onScroll
이벤트에서 받아볼 수 있고, 너무 많은 이벤트가 trigger 되는 것을 방지하기 위해 scrollEventThrottle
옵션을 추가합니다.
Animated
와 React
를 import합니다. React
의 useRef
만 import해도 됩니다.import React, { useRef } from 'react';
import { Animated } from 'react-native';
ScrollView
의 현재 스크롤 위치를 저장해둘 변수를 선언합니다.const scrolling = React.useRef(new Animated.Value(0)).current;
// or
const scrolling = useRef(new Animated.Value(0)).current;
onScroll
이벤트의 콜백함수를 정의합니다. 이 함수에서는 위에서 선언한 scrolling
변수에 스크롤의 현재 위치값을 저장합니다.const onScroll = (e) => {
const position = e.nativeEvent.contentOffset.y;
scrolling.setValue(position);
};
Animated.ScrollView
컴포넌트를 구성합니다. 컴포넌트 내부가 비어있으면 스크롤이 생기지않으니 더미 컴포넌트로 채워줍니다.<Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
{
Array(20).fill(0).map((_, index) => {
return (
<View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
<Text style={{color: 'black'}}>{`item${index}`}</Text>
</View>
);
})
}
</Animated.ScrollView>
여기까지의 전체 코드는 아래처럼 작성할 수 있습니다.
import React from 'react';
import { Animated } from 'react-native';
const App = () => {
// scrolling: 현재 스크롤의 위치입니다. 초기값은 0입니다.
const scrolling = React.useRef(new Animated.Value(0)).current;
const onScroll = (e) => {
// position: 스크롤의 y축(세로) 위치입니다.
const position = e.nativeEvent.contentOffset.y;
scrolling.setValue(position);
};
return (
// 10 프레임당 한 번 onScroll 이벤트를 발생시킵니다.
<Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
{
Array(20).fill(0).map((_, index) => {
return (
<View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
<Text style={{color: 'black'}}>{`item${index}`}</Text>
</View>
);
})
}
</Animated.ScrollView>
);
}
export default App;
Header
컴포넌트는 View
컴포넌트를 이용해서 구성할 수 있습니다. 하지만 Animated
를 적용하려면 ScrollView
의 경우와 마찬가지로, RN에서 기본으로 제공되는 View
컴포넌트를 사용하는 대신에 Animated.View
컴포넌트를 사용해야 합니다.
Header
컴포넌트의 height
style에 적용할 변수를 선언합니다. 위에서 선언한 scrolling
변수의 interpolate
메소드로 선언합니다. inputRange
는 scrolling
값의 범위를 표현하는 배열이고 outputRange
는 scrolling
값에 따라 생성될 값의 범위를 표현하는 배열입니다. 아래의 코드에서는 scrolling
이 0인 경우에 height
는 200이 되고, scrolling
이 200인 경우에 height
는 50이 됩니다. ( 따라서 Scrolling
이 100인 경우에는 height
에 200과 50의 중간값이 적용됩니다. )const height = scrolling.interpolate({
inputRange: [0, 200],
outputRange: [200, 50],
extrapolate: 'clamp',
});
Header
컴포넌트를 구성합니다. 위에서 선언한 height
를 style에 적용합니다. 컴포넌트의 style은 StyleSheet
를 이용하여 적용해도 됩니다.<Animated.View style={{ height, backgroundColor: 'orange' }}>
</Animated.View>
여기까지의 전체 코드는 아래처럼 작성할 수 있습니다.
import React from 'react';
import { Animated } from 'react-native';
const App = () => {
// scrolling: 현재 스크롤의 위치입니다. 초기값은 0입니다.
const scrolling = React.useRef(new Animated.Value(0)).current;
// height: 스크롤의 위치에 따른 Header 컴포넌트의 높이값입니다. 스크롤 위치가 0부터 200으로 변함에 따라 높이값은 200부터 50으로 비율에 맞춰 변합니다.
const height = scrolling.interpolate({
inputRange: [0, 200],
outputRange: [200, 50],
extrapolate: 'clamp',
});
const onScroll = (e) => {
// position: 스크롤의 y축(세로) 위치입니다.
const position = e.nativeEvent.contentOffset.y;
scrolling.setValue(position);
};
return (
<View>
<Animated.View style={{ height, backgroundColor: 'orange' }}>
</Animated.View>
// 10 프레임당 한 번 onScroll 이벤트를 발생시킵니다.
<Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
{
Array(20).fill(0).map((_, index) => {
return (
<View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
<Text style={{color: 'black'}}>{`item${index}`}</Text>
</View>
);
})
}
</Animated.ScrollView>
</View>
);
}
export default App;
Animated
는 View
뿐만 아니라 Image
, Text
, FlatList
, SectionList
등에 적용할 수 있습니다. 여기에서는 Text
에 적용해보겠습니다.
Text
의 fontSize
style에 적용할 변수를 선언합니다. 위에서 선언한 height
와 같은 방식으로 선언하면 됩니다. height
와 마찬가지로, scrolling
이 0인 경우에 fontSize
는 32이고 scrolling
이 200인 경우에 fontSize
는 16입니다.const fontSize = scrolling.interpolate({
inputRange: [0, 200],
outputRange: [32, 16],
extrapolate: 'clamp',
});
Header
컴포넌트의 child로 Animated.Text
컴포넌트를 추가합니다. 위에서 선언한 fontSize
변수를 style에 적용합니다. 컴포넌트의 style은 StyleSheet
를 이용하여 적용해도 됩니다. ( 정렬을 위해 Header
컴포넌트에 alignItems
와 justifyContent
속성을 center
로 설정하였습니다. )<Animated.View style={{ height, alignItems: 'center', justifyContent: 'center', backgroundColor: 'orange' }}>
<Animated.Text style={{ fontSize, color: 'black' }}>Animated Header</Animated.Text>
</Animated.View>
여기까지의 전체 코드는 아래처럼 작성할 수 있습니다.
import React from 'react';
import { Animated } from 'react-native';
const App = () => {
// scrolling: 현재 스크롤의 위치입니다. 초기값은 0입니다.
const scrolling = React.useRef(new Animated.Value(0)).current;
// height: 스크롤의 위치에 따른 Header 컴포넌트의 높이값입니다. 스크롤 위치가 0부터 200으로 변함에 따라 높이값은 200부터 50으로 비율에 맞춰 변합니다.
const height = scrolling.interpolate({
inputRange: [0, 200],
outputRange: [200, 50],
extrapolate: 'clamp',
});
// fontSize: 스크롤의 위치에 따른 Text 컴포넌트의 폰트 크기값입니다. 스크롤 위치가 0부터 200으로 변함에 따라 폰트 크기값은 32부터 16으로 비율에 맞춰 변합니다.
const fontSize = scrolling.interpolate({
inputRange: [0, 200],
outputRange: [32, 16],
extrapolate: 'clamp',
});
const onScroll = (e) => {
// position: 스크롤의 y축(세로) 위치입니다.
const position = e.nativeEvent.contentOffset.y;
scrolling.setValue(position);
};
return (
<View>
<Animated.View style={{ height, alignItems: 'center', justifyContent: 'center', backgroundColor: 'orange' }}>
<Animated.Text style={{ fontSize, color: 'black' }}>Animated Header</Animated.Text>
</Animated.View>
// 10 프레임당 한 번 onScroll 이벤트를 발생시킵니다.
<Animated.ScrollView scrollEventThrottle={10} onScroll={onScroll}>
{
Array(20).fill(0).map((_, index) => {
return (
<View key={index} style={{ height: 100, marginVertical: 10, backgroundColor: 'lightgray' }}>
<Text style={{color: 'black'}}>{`item${index}`}</Text>
</View>
);
})
}
</Animated.ScrollView>
</View>
);
}
export default App;
React Native의 Animated API 로 구현할 수 있는 애니메이션은 정말 다양합니다. 서비스에 적절한 애니메이션을 적용하는 것은 서비스를 좀 더 멋지게 만들어줄 수 있는 하나의 방법인 것 같습니다.