본문 바로가기

리액트 네이티브

리액트 네이티브 스크롤뷰 중첩

안드로이드의 경우 네이티브 NestedScrollView를 사용하고, iOS는 네이티브 스크롤뷰를 조작하여 내부 스크롤 뷰와 함께 작동할 수 있게 만든 스크롤뷰입니다.

 

react-native-troika/packages/nested-scroll at master · sdcxtech/react-native-troika

Native UI Component for React Native, including nested-scroll, pull-to-refresh, bottom-sheet, etc. - sdcxtech/react-native-troika

github.com

 

안드로이드

리액트 네이티브의 ScrollView 컴포넌트는 네이티브 ScrollView 뷰를 제어하는 컴포넌트입니다.

 

기본 ScrollView는 중첩되면 내부 ScrollView가 스크롤 이벤트를 먼저 소비하기 때문에, 바깥 ScrollView가 스크롤을 인식하지 못합니다. (참고: [Android] NestedScrollView에 대해 알아보자!)

 

반면 NestedScrollViewonNestedPreScroll, onNestedPreFling 메서드를 오버라이드하여 자식 스크롤뷰의 스크롤, Fling(손가락 튕기는 제스쳐) 이벤트를 가로챌 수 있습니다.

https://developer.android.com/reference/androidx/core/widget/NestedScrollView#onNestedPreScroll(android.view.View,int,int,int[])

 

nested-scrollNestedScrollView를 제어하는 컴포넌트입니다.

 

움직인 y(dy)에서 자식의 스크롤값(comsumed[1])을 뺀 값이 0보다 크다는 것은 자식은 맨 위로 올라갔고, 그만큼 자신의 스크롤을 올립니다.

public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
  super.onNestedPreScroll(target, dx, dy, consumed, type);
  int dyUnconsumed = dy - consumed[1]; // 요기 !!
  if (dyUnconsumed > 0) { // 요기 !!
    final int oldScrollY = getScrollY();
    scrollBy(0, dyUnconsumed); // 요기 !!
    final int myConsumed = getScrollY() - oldScrollY;
    consumed[1] += myConsumed;
  }
}

https://github.com/sdcxtech/react-native-troika/blob/e65a0f6b313aceed288197ba61fa9cbc1dc1d451/packages/nested-scroll/android/src/main/java/com/reactnative/nestedscroll/NestedScrollView.java#L44-L53

 

iOS

제스쳐가 인식되면 gestureRecognizer가 호출됩니다.

 

매개변수인 touch 객체의 view에 터치된 위치의 뷰가 들어 있습니다.

https://github.com/sdcxtech/react-native-troika/blob/e65a0f6b313aceed288197ba61fa9cbc1dc1d451/packages/nested-scroll/ios/NestedScrollView/RNNestedScrollView.m#L57-L67

 

부모 뷰로 올라가면서 스크롤 가능한 뷰(내부 스크롤뷰)를 찾습니다.

https://github.com/sdcxtech/react-native-troika/blob/e65a0f6b313aceed288197ba61fa9cbc1dc1d451/packages/nested-scroll/ios/NestedScrollView/RNNestedScrollView.m#L81

 

스크롤 가능한 뷰라면 자신을 contentOffset 속성에 대한 observer로 등록합니다.

https://github.com/sdcxtech/react-native-troika/blob/e65a0f6b313aceed288197ba61fa9cbc1dc1d451/packages/nested-scroll/ios/NestedScrollView/RNNestedScrollView.m#L77

 

내부 스크롤뷰의 contentOffset이 변경되면 observeValueForKeyPath 메서드가 호출됩니다.

https://github.com/sdcxtech/react-native-troika/blob/e65a0f6b313aceed288197ba61fa9cbc1dc1d451/packages/nested-scroll/ios/NestedScrollView/RNNestedScrollView.m#L221

 

gestureRecognizer에서 YES를 반환하면 내부 스크롤뷰를 스크롤해도 자신도 스크롤되면서 scrollViewDidScroll 메서드가 호출됩니다.

https://github.com/sdcxtech/react-native-troika/blob/e65a0f6b313aceed288197ba61fa9cbc1dc1d451/packages/nested-scroll/ios/NestedScrollView/RNNestedScrollView.m#L170

 

observeValueForKeyPath 메서드와 scrollViewDidScroll 메서드에서 자신과 내부 스크롤뷰의 위치를 조정합니다.

 

이 부분만 보겠습니다.

CGFloat dy = old - new;
    
if (dy < 0) {
    if (lt(main.contentOffset.y, self.headerScrollRange) && target.contentOffset.y > 0) {
        _nextReturn = true;
        target.contentOffset = CGPointMake(0, fmax(0, old));
        return;
    }
    // ...
}

https://github.com/sdcxtech/react-native-troika/blob/e65a0f6b313aceed288197ba61fa9cbc1dc1d451/packages/nested-scroll/ios/NestedScrollView/RNNestedScrollView.m#L234-L243

 

dy0보다 작다는 것은 스크롤을 아래로 내린 것입니다.

 

이때 두 스크롤뷰가 아래로 내려가는데, 외부 스크롤뷰가 헤더 범위까지 먼저 내려가야 하므로, 내부 스크롤뷰의 위치를 원래대로 돌려서 위치를 고정시킵니다.

 

제약사항으로는 NestedScrollViewHeader를 제외한 한 개의 자식만 랜더링할 수 있습니다.