React Native Top Sheet 직접 구현 #2

2024. 8. 30. 22:54·🔍 Tech 🔍/Front-End

지난번에 gesture handler, reanimated 라이브러리를 사용해 top sheet를 직접 구현해 보았는데, 오늘은 top sheet가 확장/축소 됐을 때 다른 컨텐츠를 보여주고 하단에 자연스럽게 스크롤이 생기게 적용했다.

 

1. 컨텐츠 배치

top sheet 아래 부분에 메인 컨텐츠가 표시되어야 하므로 Section 컴포넌트를 ScrollView로 묶어주었다.

 

<>
  <TopSheet />
  <ScrollView style={styles.scrollViewContainer}>
      <Section1 />
      <Section2 />
  </ScrollView>
</>

 

현재 상태(top sheet가 축소된 상태)에서는 Section에 스크롤이 생기지 않는다.

하지만 확장되면 늘어난 top sheet의 높이만큼 스크롤이 생긴다.

 

 

 

 


2. 확장/축소 컨텐츠

위 사진처럼 서로 다른 컨텐츠를 자연스럽게 변환하기 위해 몇 가지 기능을 추가해야 했다.

 

2-1. useSharedValue

첫 번째로는 opacity를 사용한 전환, top sheet가 얼마나 확장되고 축소되는지에 따라 opacity를 적용해야 하므로 height의 추적이 필요하다.

 

  // 최소 높이 = 축소
  const minSheetHeight = height / 6;
  // 최대 높이 = 확장
  const maxSheetHeight = height / 2;

  // top sheet height 공유
  const sheetHeight = useSharedValue<number>(minSheetHeight);
  // gesture y 좌표
  const context = useSharedValue<{y: number}>({y: 0});

 

top sheet의 최소/최대 높이를 설정하고, 동적 style 적용을 위한 현재 top sheet의 height와 드래그가 시작되었을 때 y 값을 측정하기 위해 두 값을 useSharedValue로 생성한다.

 

2-2. useAnumatedStyle

두 번째는 선언된 공유 값을 활용해서 동적 style을 생성한다.

  // top sheet의 높이를 변경
  const animatedStyles = useAnimatedStyle(() => {
    return {
      height: sheetHeight.value,
    };
  });

  // 축소된 top sheet에 적용할 동적 style
  const animatedOpacityShort = useAnimatedStyle(() => {
    const minSheetHeight = height / 6;
    const maxSheetHeight = height / 2.4;
    const opacityRange = maxSheetHeight - minSheetHeight;

    let opacity: number = (maxSheetHeight - sheetHeight.value) / opacityRange;

    opacity = Math.max(0, Math.min(opacity, 1));

    const zIndex: number = opacity === 1 ? 10 : -10;

    return {
      opacity,
      zIndex,
    } as { opacity: number; zIndex: number };
  });

   // 확장된 top sheet에 적용할 동적 style
  const animatedOpacityLong = useAnimatedStyle(() => {
    const opacityRange = maxSheetHeight - minSheetHeight;

    let opacity: number = (maxSheetHeight - sheetHeight.value) / opacityRange;

    opacity = 1 - Math.max(0, Math.min(opacity, 1));

    return {
      opacity,
    } as { opacity: number;};
  });

 

opacity 값은 최대 높이(maxSheetHeight)에서 현재 높이(sheetHeight.value)를 뺀 값이 되는데, animatedOpacityShort에 maxSheetHeight값을 다르게 설정한 이유는 top sheet가 90%쯤 확장되었을 때 opacity가 0이 되었으면 해서이다.

(height / 2 > height / 2.4)

 

maxSheetHeight값을 원래 높이(height / 2)와 같게 설정하면, 100% 확장(height / 2)되었을 때 opacity가 0이 되고 더 작은 값(height / 2.4)으로 설정하게 되면 height / 2.4 지점에 도달했을 때 opacity가 0이 되는 것이다.

 

2-3. Gesture

그리고 마지막으로 gesture에 대한 핸들러 함수를 추가해야 한다.

  const gesture = Gesture.Pan()
  	// 드래그 시작
    .onStart(() => {
      context.value = {y: sheetHeight.value};
    })
    
    // 드래그 중
    .onUpdate((e) => {
      const newValue = e.translationY + context.value.y;
      sheetHeight.value = Math.min(
        Math.max(newValue, minSheetHeight),
        maxSheetHeight,
      );
    })
    
    // 드래그 종료
    .onEnd((e) => {
      if (e.velocityY > 0) {
        sheetHeight.value = withSpring(maxSheetHeight, {
          damping: 10,
          stiffness: 400,
          overshootClamping: true,
        });
      } else {
        sheetHeight.value = withSpring(minSheetHeight, {
          damping: 10,
          stiffness: 400,
          overshootClamping: true,
        });
      }
    });

  return (
    <Animated.View style={[styles.containerOut, animatedStyles]}>
      <GestureHandlerRootView>
        <GestureDetector gesture={gesture}>
          <Animated.View style={[styles.container, animatedStyles]}>
            <TopSheetShort
              animatedOpacityShort={animatedOpacityShort}
            />
            <TopSheetLong
              animatedOpacityLong={animatedOpacityLong}
            />
            <TopSheetBtn toggleHeight={toggleHeight} />
          </Animated.View>
        </GestureDetector>
      </GestureHandlerRootView>
    </Animated.View>
  );

 

onStart에서 드래그가 시작될 때 sheetHeight의 값을 저장하고, onUpdate에서 손가락이 움직인 y축 거리만큼 sheetHeight에 더해주게 된다. 범위는 초기에 설정한 top sheet max/minSheetHeight안에서만 유효하다.

 

onEnd 핸들러는 사용자가 위로 드래그(e.velocityY < 0)했는지 또는 아래로 드래그(e.velocityY > 0)했는지 감지해 손가락을 도중에 놓더라도 확장/축소가 가능하게 한다. 

 

damping, stiffness, overshootClamping은 react native reanimated 라이브러리에서 지원하는 옵션들이다.
각각 애니메이션 정지 속도, 애니메이션 속도, 반동을 의미한다.
그 외 옵션과 자세한 내용은 라이브러리 페이지에서 확인 가능

 

https://docs.swmansion.com/react-native-reanimated/docs/animations/withSpring

 

withSpring | React Native Reanimated

withSpring lets you create spring-based animations.

docs.swmansion.com

 

top sheet에 관한 자료나 내용이 없다 보니 필요한 분들에게 도움이 되었으면 하는 마음으로 마칩니다.

 

서로서로 도와요

 

해당 포스팅에서 사용한 예제 소스코드는 아래 repositorie에서 확인 가능합니다.

 

* 만약 해당 코드를 직접 사용하고자 한다면, gesture handler, reanimated 라이브러리를 사용하기 위해 초기 설정, 네이티브 설정이 필요하므로 꼭 확인해 주시길 바랍니다.

 

https://github.com/yeonhub/react-native-top-sheet_example

 

GitHub - yeonhub/react-native-top-sheet_example

Contribute to yeonhub/react-native-top-sheet_example development by creating an account on GitHub.

github.com

 

'🔍 Tech 🔍/Front-End' 카테고리의 다른 글
  • React Native Expo google map 지도 구현 방법
  • React Native Expo에 google AdMob 추가하기
  • React Native Top Sheet 직접 구현 #1
  • Next.js 맛보기
Yeonhub
Yeonhub
✨ https://github.com/yeonhub 📧 lsy3237@gmail.com
  • Yeonhub
    비 전공자의 Be developer
    Yeonhub
  • 전체
    오늘
    어제
    • 전체보기 (169)
      • 🔍 Tech 🔍 (19)
        • Front-End (11)
        • Back-End (4)
        • AI (1)
        • Server (1)
        • Etc (2)
      • 💡 원티드 프리온보딩 챌린지 💡 (14)
        • PRE-ONBOARDING_AI (11월) (1)
        • PRE-ONBOARDING_FE (2월) (2)
        • PRE-ONBOARDING_FE (1월) (2)
        • PRE-ONBOARDING_FE (12월) (9)
      • 🔥 부트캠프-웹 개발 🔥 (118)
        • HTML5 (7)
        • CSS3 (21)
        • JavaScript (27)
        • JavaScript_advanced (9)
        • React (24)
        • Next (1)
        • MYSql (5)
        • Node (5)
        • 오늘하날(개인프로젝트) (12)
        • 이젠제주투어(팀프로젝트) (7)
      • 💻 CS 💻 (1)
        • 알고리즘 (1)
      • ⚡ 코딩테스트 ⚡ (11)
        • JavaScript (11)
      • 📚 Books 📚 (6)
        • 클린 아키텍처 (2)
        • 인사이드 자바스크립트 (4)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    expo map
    expo node fcm
    expo fcm push
    react native firebase analytics
    컴파운드 컴포넌트 패턴
    프론트엔드 테스트코드
    node fcm
    Node
    expo 길찾기
    php node
    expo 지도
    expo admob
    node cron
    react native bottom sheet
    bottom sheet
    react native analytics
    react native expo fcm
    라스콘
    라스콘4
    expo google map
    expo fcm
    rn admob
    javascript fcm
    node crontab
    rn bottom sheet
    node.js fcm
    react vite
    python node
    expo deep linking
    react native admob
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Yeonhub
React Native Top Sheet 직접 구현 #2
상단으로

티스토리툴바