SwiftUI에는 navigationTitle
이라는 것이 있다. 위 그림의 "요약"처럼 처음에는 큼지막하게 보이지만, 사용자가 스크롤 다운하면 보통의 네비게이션 헤더처럼 상단에 작게 표시되는 컴포넌트다. SwiftUI에서는 NavigationView
라는 View 구조체에 임베드된 View 구조체에 .navigationTitle()
을 사용해서 정의하면, 위 그림과 같이 표시되게 된다.
이를 React Native에서 Animated
를 이용해 구현해보았다.
여러 검색을 해본 결과, 다음과 같은 방법이 유효할 듯 싶다.
이를 위해서는 일단, 사용자의 스크롤링 위치를 추적하기 위해 scrollY
라는 Animated.View
타입의 변수가 필요하다. 이 변수는 뷰의 리랜더링에 영향을 받으면 안되니, useRef
를 이용해 정의해준다.
const scrollY = useRef(new Animated.Value(0)).current
사용자의 스크롤링 위치(offset)를 이 변수에 매핑하기 위해서는 ScrollView
의 onScroll
속성에 작업이 필요하다. onScroll
에는 Animated.event()
의 반환값이 매핑되는데, 이를 위해서는 Animated.ScrollView
가 필요하다.
<Animated.ScrollView
scrollEventThrottle={16}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], { useNativeDriver: true })}>
{largeHeader}
{children}
</Animated.ScrollView>
Animated.event
함수는 첫번째 파라미터에 정의된 값을 자동으로 setValue
함수를 통해 Animated.View
변수에 매핑시켜준다. 위의 경우, contentOffset.y
의 값이 scrollY
변수에 매핑되게 된다.
이제 사용자 스크롤링 값에 따라 작은 헤더의 투명도를 변경해야 한다. 사용자의 스크롤링 값은 scrollY
에 기록되고 있다. scrollY
값에 따라 특정한 투명도 값을 계산해야 한다. 이렇게 특정 Animated.Value
값의 변화에 따라 정해진대로 값을 계산해주는 함수는 interpolate
함수이다.
const headerOpacity = scrollY.interpolate({
inputRange: [0, headerHeight],
outputRange: [0, 1],
})
inputRange
는 scrollY
의 값을 의미한다. outputRange
는 interpolate
함수의 결과값인 headerOpacity
가 갖게될 값을 의미한다. 그래서 위의 코드를 해석하면, scrollY
가 0
일 때, headerOpacity
는 0
을 갖고, scrollY
가 headerHeight
일 때, headerOpacity
는 1
을 갖는다. 0
~ 1
사이에는 자동으로 부드럽게 애니메이션 처리가 된다. (계속 0이다가 갑자기 1로 커지는 것이 아니다)
그래서 이 headerOpacity
값을 작은 헤더의 style
에 넣어주면 된다.
<Animated.View
style={{
height: headerHeight,
opacity: headerOpacity,
}}>
{smallHeader}
</Animated.View>
이로서 사용자의 스크롤링 위치에 따라 작은 헤더의 투명도가 0에서 1까지 조절된다. 사용자에게는 작은 헤더가 나타났다 사라졌다 하는 것으로 보일 것이다.
이 과정을 AnimatedHeader
라는 이름의 React 컴포넌트로 만들었는데, 전체 코드는 다음과 같다.
import React, { ReactNode, useRef } from 'react'
import { Animated } from 'react-native'
interface AnimatedHeaderProps {
children: ReactNode
smallHeader: ReactNode
largeHeader: ReactNode
headerHeight: number
}
function AnimatedHeader({
children,
smallHeader,
largeHeader,
headerHeight,
}: AnimatedHeaderProps): JSX.Element {
const scrollY = useRef(new Animated.Value(0)).current
const headerOpacity = scrollY.interpolate({
inputRange: [0, headerHeight],
outputRange: [0, 1],
})
return (
<>
<Animated.View
style={{
height: headerHeight,
opacity: headerOpacity,
}}
>
{smallHeader}
</Animated.View>
<Animated.ScrollView
scrollEventThrottle={16}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
useNativeDriver: true,
})}
>
{largeHeader}
{children}
</Animated.ScrollView>
</>
)
}
export default AnimatedHeader