React Native에서 SwiftUI의 navigation title 흉내내기

byron1st·2021년 3월 20일
0
post-thumbnail

SwiftUI에는 navigationTitle 이라는 것이 있다. 위 그림의 "요약"처럼 처음에는 큼지막하게 보이지만, 사용자가 스크롤 다운하면 보통의 네비게이션 헤더처럼 상단에 작게 표시되는 컴포넌트다. SwiftUI에서는 NavigationView 라는 View 구조체에 임베드된 View 구조체에 .navigationTitle()을 사용해서 정의하면, 위 그림과 같이 표시되게 된다.

이를 React Native에서 Animated 를 이용해 구현해보았다.

방법 설계

여러 검색을 해본 결과, 다음과 같은 방법이 유효할 듯 싶다.

  1. 우선 최상단의 작은 헤더(우측 그림)와 큰 헤더(좌측 그림) 컴포넌트를 모두 그린다.
  2. 스크롤에 따라 작은 헤더의 투명도 값을 조절하여 보이거나 가린다.

사용자 스크롤링 감지

이를 위해서는 일단, 사용자의 스크롤링 위치를 추적하기 위해 scrollY 라는 Animated.View 타입의 변수가 필요하다. 이 변수는 뷰의 리랜더링에 영향을 받으면 안되니, useRef를 이용해 정의해준다.

const scrollY = useRef(new Animated.Value(0)).current

사용자의 스크롤링 위치(offset)를 이 변수에 매핑하기 위해서는 ScrollViewonScroll 속성에 작업이 필요하다. 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],
  })

inputRangescrollY의 값을 의미한다. outputRangeinterpolate 함수의 결과값인 headerOpacity가 갖게될 값을 의미한다. 그래서 위의 코드를 해석하면, scrollY0일 때, headerOpacity0을 갖고, scrollYheaderHeight 일 때, headerOpacity1을 갖는다. 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
profile
Fullstack software engineer specialized for Blockchain

0개의 댓글