
어느 날인가 문득 무신사 앱에 위치한 - 사람에 따라 이미지 슬라이더라고 부르기도 하는 - Carousel(캐러셀)이 눈길을 사로잡았다. 별 생각없이 이미지를 넘기다 뭔가 기존과 다른 느낌을 받았기 때문이다.
일반적인 캐러셀은 아래와 같이 양 옆 이미지가 가운데 위치한 이미지를 밀어내며 이동한다.
ex) react-native-reanimted-carousel, normal-horizontal mode
팀에서 그래프 애니메이션과 UX 마이크로 인터랙션 등 애니메이션 효과를 가장 좋아해서 주로 개발하는 나는 Reanimated를 통해 해당 컴포넌트를 재현해보면 재밌겠다는 생각이 들었다. 그래서 masked view를 사용하여 무신사의 캐러셀처럼 동작하는 react-native-masked-carousel 를 만들어 보기로 했다.
<GestureDetector gesture={onDragGesture}> // onDragGesture -> Panning 이벤트 리스너
//...캐러셀 동작 부분
</GestureDetector>
{...}
{/* CENTER IMAGE */}
<View>
<FastImage // Image cache를 위해 react-native-fast-image 사용
...
/>
</View>
{/* LEFT IMAGE */}
<MaskedView
...
maskElement={ // 제스처 이벤트에 따라 움직이는 부분
<Animated.View
style={{
...animatedStylesLeft, // 마스킹 영역을 움직이기 위한 AnimatedStyle
}}
>
...
</Animated.View>
}
>
<FastImage />
</MaskedView>
{/* RIGHT IMAGE */}
<MaskedView
{...props}
>
{...}
</MaskedView>
{...}
텍스트를 넣기 전 Carousel
![]()
{/* CENTER TITLE, TEXT */}
// optinoal Prop인 title/subtitle이 있을 때만 출력
{data[centerImageIndex].title ? (
<Animated.View>
<Text>
{data[centerImageIndex].title}
</Text>
</Animated.View>
) : null}
{data[centerImageIndex].subTitle ? (
<Animated.View>
<Text>
{data[centerImageIndex].subTitle}
</Text>
</Animated.View>
) : null}
{/* LEFT TITLE, TEXT */}
{...}
{/* RIGHT TITLE, TEXT */}
{...}
const distanceRatioFromCenter = 1.4 // title이 이미지와 움직이는 속도를 다르게 하기 위함
const dragGesture = Gesture.Pan()
.onUpdate((e) => {
// 제스처를 통해 MaskedView가 이동한 정도를 계산 -> 시작 값 + 움직인 값
offsetXValue = startXValue + translationXValue;
// 지정된 위치를 벗어나는 경우 MaskedView 및 텍스트를 움직이지 않도록 한다.
if (offsetXValue < -width) {
offsetXValue = -width;
} else if (offsetXValue > width) {
offsetXValue = width;
}
})
.onEnd((e) => {
// Fling 제스처일때 (판단기준 -> 제스쳐 속도가 일정 기준 이상일때)
if (Math.abs(velocityX) > 500) {
if (velocityX > 0 && isLeft) { // 왼쪽 이미지가 중앙으로 이동했을 경우
// MaskedView 이동
offsetXValue = width
//이미지 교체
centerImageIndex = centerImageIndex - 1 < 0 ? data.length - 1 : centerImageIndex - 1; // for infinite loop
// 텍스트 이동
offsetXTextValue = width * distanceRatioFromCenter;
return;
}
if (velocityX < 0 && isRight) { // 오른쪽 이미지가 중앙으로 이동했을 경우
...
return;
} else {
...
}
} else { // swipe 제스쳐 일때
if (isLeft && offsetXValue > 0) {
if (offsetXValue > width / 2) { // 화면 절반 이상 swipe 된 경우
...
} else {
...
}
return;
}
if (isRight && offsetXValue < 0) {
...
} else {
...
}
return;
}
}
});
텍스트까지 넣고 난 뒤 아래처럼 무신사의 Carousel을 따라만든 react-native-masked-carousel이 완성되었다.
소스코드 전체는 여기에서 확인가능하다!
이런 유용한 정보를 나눠주셔서 감사합니다.