728x90
반응형
이번 앱 디자인에는 animated 를 많이 사용한 UI 가 들어간다.
animated view 의 핵심은, state로 ui를 변화시키는 것을 지양하고,
const animatedY = useRef(new Animated.Value(0)).current;
처럼 ref로 관리해줘야 리랜더링을 방지하여 매끄러운 애니메이션 효과가 나온다.
기본 사용은
const translateY = animatedY.interpolate({
inputRange: [0, bannerHeight - headerHeight - safeAreaTop],
outputRange: [0, -bannerHeight + headerHeight + safeAreaTop],
extrapolate: 'clamp',
});
이런 형태로, translate를 한번해주어, 스크롤 input과 output 의 레인지를 계산해주고(해당 코드는 헤더가 스크롤할때 아래로 이동하도록 하는 코드이다)
clamp로 묶어주어, input range 의 밖의 값이 입력되었을때, output range의 큰 값이 출력되도록 해준다.
애니메이션 효과를 입히고 싶은 컴포넌트에
<Animated.View
style={{
flexDirection: 'column',
transform: [{ translateY: animatedY }],
}}
>
해당 형태로 추가해준다.
animated.timing 으로 시작한 애니메이션도 있고, scroll을 통한 offset으로 해당하는 애니메이션도 있어 풀 소스코드를 올려놓는다.
const safeAreaTop = useSafeAreaInsets().top;
를 통해 ios safeArea에도 대처했다.
import { useCallback, useRef } from 'react';
import {
Animated,
Dimensions,
NativeScrollEvent,
NativeSyntheticEvent,
Platform,
Text,
UIManager,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { heightScale } from '../../util/Layout';
import { HomeHeader } from './HomeHeader';
const DUMMY = [
{
index: 0,
},
{
index: 1,
},
{
index: 2,
},
{
index: 3,
},
{
index: 4,
},
{
index: 5,
},
{
index: 6,
},
{
index: 7,
},
];
const deviceWidth = Dimensions.get('window').width;
const bannerHeight = (deviceWidth * 600) / 480;
const headerHeight = heightScale(75);
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
export const HomeScreen = () => {
let currentOffset = 0;
const animatedY = useRef(new Animated.Value(0)).current;
const animatedBackground = useRef(new Animated.Value(0)).current;
const animatedOpacity = useRef(new Animated.Value(0)).current;
const safeAreaTop = useSafeAreaInsets().top;
const animationStart = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
let direction =
event.nativeEvent.contentOffset.y > currentOffset ? 'down' : 'up';
currentOffset = event.nativeEvent.contentOffset.y;
if (
direction === 'down' &&
event.nativeEvent.contentOffset.y > bannerHeight / 3
) {
console.log('down');
Animated.parallel([
Animated.timing(animatedBackground, {
useNativeDriver: true,
duration: 300,
toValue: 1,
}),
Animated.timing(animatedOpacity, {
useNativeDriver: true,
duration: 300,
toValue: 1,
}),
]).start();
}
if (
direction === 'up' &&
event.nativeEvent.contentOffset.y < bannerHeight / 3
) {
console.log('up');
Animated.parallel([
Animated.timing(animatedBackground, {
useNativeDriver: true,
duration: 300,
toValue: 0,
}),
Animated.timing(animatedOpacity, {
useNativeDriver: true,
duration: 300,
toValue: 0,
}),
]).start();
}
},
[],
);
// const transY = Animated.
const translateY = animatedY.interpolate({
inputRange: [0, bannerHeight - headerHeight - safeAreaTop],
outputRange: [0, -bannerHeight + headerHeight + safeAreaTop],
extrapolate: 'clamp',
});
const translateHeaderY = animatedY.interpolate({
inputRange: [0, bannerHeight - headerHeight - safeAreaTop],
outputRange: [0, bannerHeight - headerHeight - safeAreaTop],
extrapolate: 'clamp',
});
const onScroll = Animated.event(
[{ nativeEvent: { contentOffset: { y: animatedY } } }],
{
useNativeDriver: true,
listener: (event) => {
animationStart(event);
},
},
);
return (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
}}
>
<Animated.FlatList
style={{ flex: 1 }}
data={DUMMY}
ListHeaderComponent={() => {
return (
<HomeHeader
animatedY={translateY}
translateHeaderY={translateHeaderY}
safeAreaTop={safeAreaTop}
animatedBackground={animatedBackground}
bannerHeight={bannerHeight}
animatedOpacity={animatedOpacity}
headerHeight={headerHeight}
/>
);
}}
onScroll={onScroll}
// pagingEnabled
stickyHeaderIndices={[0]}
renderItem={({ item, index }) => {
return (
<View
style={{
width: 100,
height: heightScale(600),
borderWidth: 1,
backgroundColor: 'pink',
}}
>
<Text style={{ fontSize: 40 }}>{index}</Text>
</View>
);
}}
/>
</View>
);
};
import React from 'react';
import {
Animated,
Dimensions,
Image,
TouchableOpacity,
View,
} from 'react-native';
import AlarmBlack from '../../assets/icons/alarmBlack.svg';
import AlarmWhite from '../../assets/icons/alarmWhite.svg';
import { heightScale, widthScale } from '../../util/Layout';
type HomeHeaderProps = {
animatedY: Animated.AnimatedInterpolation<string | number>;
translateHeaderY: Animated.AnimatedInterpolation<string | number>;
safeAreaTop: number;
animatedBackground: Animated.Value;
bannerHeight: number;
animatedOpacity: Animated.Value;
headerHeight: number;
};
const deviceWidth = Dimensions.get('window').width;
export const HomeHeader = ({
animatedY,
translateHeaderY,
safeAreaTop,
animatedBackground,
bannerHeight,
animatedOpacity,
headerHeight,
}: HomeHeaderProps) => {
return (
<Animated.View
style={{
flexDirection: 'column',
transform: [{ translateY: animatedY }],
}}
>
<Animated.View
style={{
position: 'absolute',
top: safeAreaTop,
width: deviceWidth,
height: headerHeight,
zIndex: 1,
transform: [{ translateY: translateHeaderY }],
backgroundColor: animatedBackground.interpolate({
inputRange: [0, 1],
outputRange: ['#FFFFFF00', '#FFFFFF'],
}),
alignItems: 'center',
justifyContent: 'center',
}}
>
<Image
source={require('../../assets/images/headerLogo.png')}
style={{
width: widthScale(50),
height: heightScale(35),
}}
resizeMode="contain"
/>
<TouchableOpacity
style={{
width: headerHeight,
height: headerHeight,
position: 'absolute',
right: 0,
justifyContent: 'center',
alignItems: 'center',
top: 0,
}}
>
<Animated.View
style={{
zIndex: 2,
position: 'absolute',
opacity: animatedOpacity,
}}
>
<AlarmBlack width={widthScale(40)} height={widthScale(40)} />
</Animated.View>
<AlarmWhite width={widthScale(40)} height={widthScale(40)} />
</TouchableOpacity>
</Animated.View>
<TouchableOpacity
style={{
width: deviceWidth,
height: bannerHeight,
backgroundColor: 'orange',
}}
></TouchableOpacity>
<View
style={{
width: '100%',
height: headerHeight,
backgroundColor: 'blue',
}}
></View>
<View
style={{
width: '100%',
height: headerHeight,
backgroundColor: 'red',
}}
></View>
</Animated.View>
);
};
728x90
반응형
'ReactNative' 카테고리의 다른 글
[ React Native ] Figma font scale (0) | 2023.10.31 |
---|---|
[ React Native ] Flatlist / scrollview pagingEnabled 시, offset option (0) | 2023.10.31 |
[ React Native ] flatlist scrollview onScroll 시, position 이동 및 스크롤에 따른 데이터 변경시 함수 작성 (1) | 2023.10.27 |
[ React Native ] LayoutAnimation 을 사용한 ui 예제 (0) | 2023.10.26 |
[ React Native ] animated view 로 flatlist 스크롤시, 배경화면 투명도 바꾸기 (0) | 2023.10.26 |