본문 바로가기
코딩생활/ReactNative

[React Native] 3-8 스크롤 내렸을 때 글쓰기 버튼 숨기기

by InfoJun 2023. 3. 18.
반응형

스크롤을 내렸을 때 글쓰기 버튼을 숨기기 위해서는

 

일단 스코롤이 FlatList의 바닥에 가까워졌는지 감지할 수 있어야합니다.

<FlatList
	(...)
    onEndReacthed = {(distanceFromEnd) => {
    	console.log("바닥에 가까워 졌음")
    }}
    onEndReachedThreshold={0.85}
>

 

이렇게 하면 콘텐츠의 85% 스크롤 했을 때 onEndReached 함수가 호출 됩니다.

 

스크롤로 많은 정보를 불로으는 무한스크롤을 구현할 때 이 Props 를 사용하면 유용한데

 

저희는 아직 사용하지 못합니다.

 

그래서 저희는 onEndReached를 사용하는 대신 onScrool 이벤트를 사용할 것입니다.

 

onScroll 이벤트를 사용하면 콘텐츠의 전체 크기와 스크롤의 위치를 알아낼 수 있습니다.

 

components/FeedList.js

import React from 'react';
import {View, StyleSheet, FlatList} from 'react-native';
import FeedListItem from './FeedListItem';


function FeedList({logs}) {
  // ---추가---
  const onScroll = (e) => {
    const {contentSize, layoutMeasurement, contentOffset} = e.nativeEvent;
    console.log({contentSize, layoutMeasurement, contentOffset})
  }
  //----
  return (
    <FlatList
      data={logs}
      style={styles.block}
      renderItem={({item}) => <FeedListItem log={item} />}
      keyExtractor={log => log.id}
      ItemSeparatorComponent={() => <View style={styles.separator} />}
      onScroll={onScroll}  //추가
    />
  );
}

(...)

 

스크롤을 내렸을 때 콘솔창에 값이 출력되는 걸 확인 할 수 있습니다.

 

{"contentOffset": {"x": 0, "y": 299}, 
"contentSize": {"height": 853, "width": 375}, 
"layoutMeasurement": {"height": 554, "width": 375}}

 

여기서 contentSize.height 는 FlatList의 내부의 전체 크기를 나타냅니다.

 

layoutMeasurement.height 는 화면에 나타난 FlatList의 실제 크기를 나타납니다.

 

contentOffset.y는 스클롤 할때마다 늘어나는 값입니다.

 

스크롤이 맨위에 있을 때는 0이고, 스크롤이 맨 아래 있을 떄는

 

contentSize.height - layoutMeasurement.height - contentOffset.y 과 같습니다.

 

components/FeddList.js - onScroll

const onScroll = e => {
  const {contentSize, layoutMeasurement, contentOffset} = e.nativeEvent;
  const distanceFromBottom =
    contentSize.height - layoutMeasurement.height - contentOffset.y;

  if (distanceFromBottom < 73) {
    console.log('바닥과 가까워짐');
  } else {
    console.log('바닥과 멀어짐');
  }
};

 

이러면 움직일 때 콘솔창에 log가 찍히는 것을 확인할 수 있습니다.

 

이제 FeedScreen에 hidden이라는 Bollean타입의 상태를 만들겠습니다.

 

그 후 onScrolledToBottom이라는 함수를 만들어서 파라미터로 받은 값과

 

상태 값이 다를 때 상태를 업데이트 하도록 구현하겠습니다.

 

screens/FeedScreen.js

import React, {useContext, useState} from 'react';
import {View, StyleSheet} from 'react-native';
import LogContext from '../contexts/LogContext';
import FloatingWriteButton from '../components/FloatingWriteButton';
import FeedList from '../components/FeedList';

function FeedsScreen() {
  const {logs} = useContext(LogContext);
  // ---추가---
  const [hidden, setHidden] = useState(false);
  const onScrolledToBottom = isBottom => {
    if (hidden !== isBottom) {
      setHidden(isBottom);
    }
  };
  // ----

  return (
    <View style={styles.block}>
      <FeedList logs={logs} onScrolledToBottom={onScrolledToBottom} /> // 수정
      <FloatingWriteButton />
    </View>
  );
}

(...)

 

components/FeedList.js

import React from 'react';
import {View, StyleSheet, FlatList} from 'react-native';
import FeedListItem from './FeedListItem';

function FeedList({logs, onScrolledToBottom}) { // 변경 
  const onScroll = e => {
    const {contentSize, layoutMeasurement, contentOffset} = e.nativeEvent;
    const distanceFromBottom =
      contentSize.height - layoutMeasurement.height - contentOffset.y;

    if (distanceFromBottom < 73) {
      onScrolledToBottom(true) //변경
    } else {
      onScrolledToBottom(false) //변경
    }
  };
  
  (...)

 

이렇게 하면 스크롤 위치에 따라 hidden 값이 변경됩니다. 

 

이제 hidden 값을 FloatingWriteButton 컴포넌트에 Props로 전달하고 애니메이션을 구현하면 됩니다.

 

screens/FeedsScreens.js - FloatingWriteButton

<FloatingWriteButton hidden={hidden} />

components/FloateingWriteButton.js

import React, {useRef, useEffect} from 'react'; // useRef, useEffect 추가
import {View, StyleSheet, Platform, Pressable, Animated} from 'react-native'; //Animated 추가
import Icon from 'react-native-vector-icons/MaterialIcons';
import {useNavigation} from '@react-navigation/native';

function FloatingWriteButton({hidden}) {
  //hidden 추가
  const navigation = useNavigation();
  const onPress = () => {
    navigation.navigate('Write');
  };
  // --- 수정 ---
  const animation = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.timing(animation, {
      toValue: hidden ? 1 : 0,
      useNativeDriver: true,
    }).start();
  }, [animation, hidden]);

  return (
    <Animated.View
      style={[
        styles.wrapper,
        {
          transform: [
            {
              translateY: animation.interpolate({
                inputRange: [0, 1],
                outputRange: [0, 88],
              }),
            },
          ],
          opacity: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [1, 0],
          }),
        },
      ]}>
      <Pressable
        style={({pressed}) => [
          styles.button,
          Platform.OS === 'ios' && {
            opacity: pressed ? 0.6 : 1,
          },
        ]}
        android_ripple={{color: 'white'}}
        onPress={onPress}>
        <Icon name={'add'} size={24} style={styles.icon} />
      </Pressable>
    </Animated.View>
  );
}
// ------
(...)

 

이제 맨 아래로 내리면 +버튼이 사라지는 것을 볼 수 있을 것입니다.

 


spring 사용하기

기존에는 timing 함수로 애니메이션 효과를 적용하였는데, 이와 비슷한 함수입니다.

 

하지만 단순히 toValue로 지정한 값으로 서서히 변하는 것이 아닌

 

마치 스프링이 통통 튀는 효과가 나타납니다.

 

components/FloatingWritButton.js

useEffect(() => {
  Animated.spring(animation, {
    toValue: hidden ? 1 : 0,
    useNativeDriver: true,
    tension: 45,
    friction: 5,
  }).start();
}, [animation, hidden]);

 

이러면 버튼이 올라올 때 위로 조금 더 올라갔다 제자리로 오는 것을 확인할 수 있습니다.

 

  • tension : 강도 (기본값 : 40)
  • friction: 감속 (기본값 : 7)
  • speed : 속도 (기본값 : 12)
  • bounciness : 탄력성 (기본값 :8)

이 옵션을 사용할 때 tension/friction을 같이 사용하거나 speed/bounciness를 같이 사용할 수 있지만

 

다른 조합으로는 사용 할 수 없습니다.

 

이 외에도 많은 옵션들이 있으니 자세한 내용은 검색해보시면 될 것 같습니다.

 

https://reactnative.dev/docs/animated#spring 

 

Animated · React Native

The Animated library is designed to make animations fluid, powerful, and painless to build and maintain. Animated focuses on declarative relationships between inputs and outputs, configurable transforms in between, and start/stop methods to control time-ba

reactnative.dev

 


예외 처리하기

이번에 구현한 버튼 숨기기 기능에 예외 처리를 해야할 상황이 두가지 있습니다.

 

첫번째는 항목 개수가 적어서 스크롤이 필요 없는 상황입니다.

 

안드로이드에서는 스크롤 없이 모든 항목을 보여줄 수 있는 상황에서 스크롤이 방지되지만

 

ios에서는 스크롤이 필요 없더라도 세로 방향으로 스와이프 하면 FlatList 내부의 내용이 움직이면서

 

onScroll 함수가 호출 됩니다.

 

따라서 contentSize.height > layoutMeasurement.height 조건을 만족 할때만 버튼을 숨기도록 해야합니다.

 

두번째는 onScrolledToBottom Props가 설정되지 않았을 때 입니다.

 

추후 캘린더 화면과 검색화면에서도 이 컴포넌트를 재사용하는데 

 

해당 화면에서는 FloatingWriteButton을 보여주지 않기 때문에 버튼을 숨기는 로직이 필요하지 않습니다.

 

components/FeedList.js - onScroll

const onScroll = e => {
  if (!onScrolledToBottom) {
    return;
  }
  const {contentSize, layoutMeasurement, contentOffset} = e.nativeEvent;
  const distanceFromBottom =
    contentSize.height - layoutMeasurement.height - contentOffset.y;

  if (
    contentSize.height > layoutMeasurement.height &&
    distanceFromBottom < 72
  ) {
    onScrolledToBottom(true);
  } else {
    onScrolledToBottom(false);
  }
};
반응형

댓글