React Native의 애니메이션 슬라이딩 탭 바

오늘은 React Native에서 슬라이딩 애니메이션으로 이 사용자 지정 탭 표시줄을 수행하는 방법을 보여 드리겠습니다.


Mateo Hrastnik이 작성한 이 놀라운 튜토리얼을 통해 필요한 것을 정확히 달성하는 데 도움이 되었습니다.




문제: 이 자습서에서 작성자는 react-native-pose라는 라이브러리를 사용하여 탭 표시줄에 애니메이션을 적용합니다. 2020년 1월 15일에 이 라이브러리의 제작자는 더 이상 유지 관리되지 않으며 더 이상 사용되지 않는다고 발표했습니다. 탭 표시줄을 애니메이션으로 만드는 다른 방법을 찾아야 합니다. 기본 Animated API를 사용하는 것이 간단하다는 것이 밝혀졌습니다. 이것이 이 튜토리얼에서 보여드릴 내용입니다. 또한 방향 변경을 관리하는 방법을 보여 드리겠습니다. 너무 게을러서 다 읽을 수 없다면 GitHub repository 으로 바로 이동할 수 있습니다.

필요한 의존성
우리는 react-navigation을 사용할 것입니다. 하단 탭 표시줄을 만들기 위해 react-navigation-tabs도 설치합니다.

yarn add react-navigation react-navigation-tabs


이 자습서에서는 아이콘에도 FontAwesome을 사용합니다. 동일한 작업을 수행하려면 필요한 모든 종속 항목을 설치해야 합니다.

yarn add react-native-svg @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-native-fontawesome


내 폴더 구성:

>src
  >components
    >icon.js
    >tabbar.js
    >router.js
  >screens
    >home.js
    >planning.js
    >search.js
    >settings.js
App.js


사용자 지정 하단 탭 표시줄 만들기



src/components/router.js에 라우터를 생성하겠습니다.

/* src/components/router.js */
import React from 'react';
import {createAppContainer} from 'react-navigation';
import {createBottomTabNavigator} from 'react-navigation-tabs';
import Home from '../screens/home';
import Settings from '../screens/settings';
import Search from '../screens/search';
import Planning from '../screens/planning';
import Icon from './icon';
import TabBar from './tabbar';

const Router = createBottomTabNavigator(
  {
    Home: {
      screen: Home,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="home" color={tintColor} />,
      },
    },
    Planning: {
      screen: Planning,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="planning" color={tintColor} />,
      },
    },
    Search: {
      screen: Search,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="search" color={tintColor} />,
      },
    },
    Settings: {
      screen: Settings,
      navigationOptions: {
        tabBarIcon: ({tintColor}) => <Icon name="settings" color={tintColor} />,
      },
    },
  },
  {
    tabBarComponent: TabBar,
    tabBarOptions: {
      activeTintColor: '#2FC7FF',
      inactiveTintColor: '#C5C5C5',
    },
  },
);

export default createAppContainer(Router);


기본적으로 이 코드에서 우리는 경로를 정의하고 탭 표시줄에 사용자 지정 TabBar 구성 요소를 사용할 것이라고 알려줍니다.

src/components/icon.js에서:

/* src/components/icon.js */
import React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome';
import {
  faHome,
  faCog,
  faSearch,
  faClock,
} from '@fortawesome/free-solid-svg-icons';

const icons = {
  home: faHome,
  search: faSearch,
  planning: faClock,
  settings: faCog,
};

const Icon = ({name, color}) => {
  return (
    <FontAwesomeIcon icon={icons[name]} style={{color: color}} size={20} />
  );
};

export default Icon;


props에서 전달한 경로의 이름에 따라 각 탭마다 아이콘이 달라집니다.

이제 App.js에서 라우터를 가져와야 합니다.

/* App.js */
import React from 'react';
import Router from './src/components/router';

const App = () => {
  return (
     <Router />
  );
};

export default App;


src/components/tabbar.js에서:

/* src/components/tabbar.js */
import React from 'react';
import {View, TouchableOpacity, StyleSheet, SafeAreaView, Dimensions} from 'react-native';

const S = StyleSheet.create({
  container: {
    flexDirection: 'row',
    height: 54,
    borderTopWidth: 1,
    borderTopColor: '#E8E8E8',
  },
  tabButton: {flex: 1, justifyContent: 'center', alignItems: 'center'},
  activeTab: {
    height: '100%',
    alignItems: 'center',
    justifyContent: 'center',
  },
  activeTabInner: {
    width: 48,
    height: 48,
    backgroundColor: '#E1F5FE',
    borderRadius: 24,
  },
});

const TabBar = props => {
  const {
    renderIcon,
    activeTintColor,
    inactiveTintColor,
    onTabPress,
    onTabLongPress,
    getAccessibilityLabel,
    navigation,
  } = props;

  const {routes, index: activeRouteIndex} = navigation.state;
  const totalWidth = Dimensions.get("window").width;
  const tabWidth = totalWidth / routes.length;

  return (
    <SafeAreaView>
      <View style={S.container}>
        <View>
          <View style={StyleSheet.absoluteFillObject}>
            <View
              style={[S.activeTab, { width: tabWidth }]}>
              <View style={S.activeTabInner} />
            </View>
          </View>
        </View>
        {routes.map((route, routeIndex) => {
          const isRouteActive = routeIndex === activeRouteIndex;
          const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;

          return (
            <TouchableOpacity
              key={routeIndex}
              style={S.tabButton}
              onPress={() => {
                onTabPress({route});
              }}
              onLongPress={() => {
                onTabLongPress({route});
              }}
              accessibilityLabel={getAccessibilityLabel({route})}>
              {renderIcon({route, focused: isRouteActive, tintColor})}
            </TouchableOpacity>
          );
        })}
      </View>
    </SafeAreaView>
  );
};

export default TabBar;


다음은 사용자 지정 탭 표시줄입니다. 지금은 다음과 같습니다.


이제 우리는 activeTab에 애니메이션을 적용해야 합니다.

활성 탭 애니메이션



선택한 탭을 나타내는 원을 애니메이션화하기 위해 react-native에 포함된 Animated 라이브러리를 사용합니다. 우리의 원은 하나의 활성 탭에서 다른 탭으로 수평으로 이동해야 하므로 translateX 변환 값을 사용할 것입니다.

src/components/tabbar.js에서:

...
/* src/components/tabbar.js */
import {useState} from 'react';
import {Animated} from 'react-native';

const TabBar = props => {
  ...
  const [translateValue] = useState(new Animated.Value(0)); 
// When the user opens the application, it's the first tab that is open.
// The initial value of translateX is 0.

  const onTabBarPress = (route, routeIndex) => {
    onTabPress(route); // function that will change the route;
    Animated.spring(translateValue, {
      toValue: routeIndex * tabWidth,
// The translateX value should change depending on the chosen route
      velocity: 10,
      useNativeDriver: true,
    }).start(); // the animation that animates the active tab circle
  };

  return (
    <SafeAreaView>
      <View style={S.container}>
        <View>
          <View style={StyleSheet.absoluteFillObject}>
            <Animated.View
              style={[
                S.activeTab,
                {
                  width: tabWidth,
                  transform: [{translateX: translateValue}],
                },
              ]}>
              <View style={S.activeTabInner} />
            </Animated.View>
 {/* the container that we animate */}
          </View>
        </View>
        {routes.map((route, routeIndex) => {
          const isRouteActive = routeIndex === activeRouteIndex;
          const tintColor = isRouteActive ? activeTintColor : inactiveTintColor;

          return (
            <TouchableOpacity
              key={routeIndex}
              style={S.tabButton}
              onPress={() => {
                onTabBarPress({route}, routeIndex);
              }}
              onLongPress={() => {
                onTabLongPress({route});
              }}
              accessibilityLabel={getAccessibilityLabel({route})}>
              {renderIcon({route, focused: isRouteActive, tintColor})}
            </TouchableOpacity>
{/* the onPress function changed. We will now use the onTabPress function that we created.
We will send the route that was selected and its index */}

          );
        })}
      </View>
    </SafeAreaView>
  );
};

export default TabBar;


그게 다야. 이제 바람직한 애니메이션이 생겼습니다. 그러나 휴대폰의 방향을 변경하면 탭의 너비가 동적으로 변경되지 않으므로 휴대폰이 깨집니다.

방향 변경 관리



이를 위해 High Order Component를 생성합니다. 문서에 쓰여진 대로 HOC는 구성 요소를 가져와서 새 구성 요소를 반환하는 함수입니다.
src/components에서 with-dimensions.js를 생성합니다.

/* src/components/with-dimensions.js */
import React, {useEffect, useState} from 'react';
import {Dimensions} from 'react-native';

const withDimensions = BaseComponent => props => {
  const [dimensions, setDimensions] = useState({
    width: Dimensions.get('window').width,
  });
//setting the initial width;


  const handleOrientationChange = ({window}) => {

    const {width} = window;
    setDimensions({width});
  };

  useEffect(() => {
    Dimensions.addEventListener('change', handleOrientationChange);
//when the component is mounted and the dimensions change, we will go to the handleOrientationChange function; 
    return () =>
      Dimensions.removeEventListener('change', handleOrientationChange);
    // when the component is unmounted, we will remove the event listener; 
  }, []);

  return (
        <BaseComponent
          dimensions={{width: dimensions.width}}
          {...props}
        />
  );
};

export default withDimensions;


이제 Tabbar 구성 요소를 withDimensions 구성 요소로 래핑해야 합니다.

/*src/components/tabbar.js*/
...
import {useEffect} from 'react';
import withDimensions from './with-dimensions';

...

const TabBar = props => {
  const {
    renderIcon,
    activeTintColor,
    inactiveTintColor,
    onTabPress,
    onTabLongPress,
    getAccessibilityLabel,
    navigation,
    dimensions,
  } = props;
// adding dimensions in props

//const totalWidth = Dimensions.get("window").width;
  const tabWidth = dimensions.width / routes.length;

  useEffect(() => {
    translateValue.setValue(activeRouteIndex * tabWidth);
  }, [tabWidth]);
// whenever the tabWidth changes, we also change the translateX value

  ...

  return (
    ...
  );
};

export default withDimensions(TabBar);


이제 노치 없이 모든 Android 휴대폰과 iPhone에서 완벽하게 작동합니다. 그러나 iPhone 10+를 사용하는 경우 가로 모드에서 버그가 발생합니다.

iPhone 10+용 SafeAreaView 버그 수정



문제는 'react-native'*의 { Dimensions }가 SafeAreaView의 너비가 아니라 기기의 전체 너비를 전송한다는 것입니다. 이를 수정하기 위해 react-native-safe-area-view을 사용합니다. 너비 계산에 포함할 수 있도록 SafeAreaView의 패딩을 파악하는 데 도움이 됩니다.

yarn add react-native-safe-area-view react-native-safe-area-context


이를 사용하려면 이 라이브러리에서 제공하는 SafeAreaProvider에서 App 구성 요소를 래핑해야 합니다.

/* App.js */

import React from 'react';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import Router from './src/components/router';

const App = () => {
  return (
    <SafeAreaProvider>
      <Router />
    </SafeAreaProvider>
  );
};

export default App;


이제 src/components/withDimensions.js에서:

/*src/components/withDimensions.js*/
...
import {SafeAreaConsumer} from 'react-native-safe-area-context';

...
return (
    <SafeAreaConsumer>
      {insets => (
        <BaseComponent
          dimensions={{width: dimensions.width - insets.left - insets.right}}
          {...props}
        />
      )}
    </SafeAreaConsumer>
  );
{/*  when we are calculating the width, we substract the padding depending on the iPhone model */}


또한 SafeAreaView 가져오기를 변경하는 것을 잊지 마십시오. 이제 'react-native-safe-area-view'에서 가져옵니다.

/*src/components/tabbar.js*/
import SafeAreaView from 'react-native-safe-area-view';


그게 다야! 이제 탭바가 모든 기기에서 작동합니다.

좋은 웹페이지 즐겨찾기