From 5e38f9ccd87c5bed9c2e8529ea6cdfe7684e493b Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Thu, 12 Apr 2018 12:13:32 +0200 Subject: [PATCH] fix: support dynamically changing routes in bottom navigation --- src/components/BottomNavigation.js | 117 ++++++++++-------- .../BottomNavigation.test.js.snap | 40 +++--- 2 files changed, 87 insertions(+), 70 deletions(-) diff --git a/src/components/BottomNavigation.js b/src/components/BottomNavigation.js index 76c7b9f..8f4dcaf 100644 --- a/src/components/BottomNavigation.js +++ b/src/components/BottomNavigation.js @@ -204,10 +204,36 @@ const MIN_SHIFT_AMOUNT = 10; const MIN_TAB_WIDTH = 96; const MAX_TAB_WIDTH = 168; const BAR_HEIGHT = 56; -const SMALL_RIPPLE_SIZE = 96; const ACTIVE_LABEL_SIZE = 14; const INACTIVE_LABEL_SIZE = 12; +const calculateShift = (activeIndex, currentIndex, numberOfItems) => { + if (activeIndex < currentIndex) { + // If the new active tab comes before current tab, current tab will shift towards right + return 2 * MIN_SHIFT_AMOUNT; + } + + if (activeIndex > currentIndex) { + // If the new active tab comes after current tab, current tab will shift towards left + return -2 * MIN_SHIFT_AMOUNT; + } + + if (activeIndex === currentIndex) { + if (currentIndex === 0) { + // If the current tab is the new active tab and its the first tab, it'll shift towards right + return MIN_SHIFT_AMOUNT; + } + + if (currentIndex === numberOfItems - 1) { + // If the current tab is the new active tab and its the last tab, it'll shift towards left + return -MIN_SHIFT_AMOUNT; + } + } + + // If the current tab is the new active tab and its somewhere in the middle, it won't shift + return 0; +}; + /** * Bottom navigation provides quick navigation between top-level views of an app with a bottom tab bar. * It is primarily designed for use on mobile. @@ -272,32 +298,49 @@ class BottomNavigation extends React.Component, State> { } static getDerivedStateFromProps(nextProps, prevState) { - const current = nextProps.navigationState.index; + const { index, routes } = nextProps.navigationState; - if (current === prevState.current) { - return null; + // Re-create animated values if routes have been added/removed + // Preserve previous animated values if they exist, so we don't break animations + const tabs = routes.map( + // focused === 1, unfocused === 0 + (_, i) => prevState.tabs[i] || new Animated.Value(i === index ? 1 : 0) + ); + const shifts = routes.map( + (_, i) => + prevState.shifts[i] || + new Animated.Value(calculateShift(index, i, routes.length)) + ); + + const nextState = { + tabs, + shifts, + }; + + if (index !== prevState.current) { + /* $FlowFixMe */ + Object.assign(nextState, { + // Store the current index in state so that we can later check if the index has changed + current: index, + previous: prevState.current, + // Set the current tab to be loaded if it was not loaded before + loaded: prevState.loaded.includes(index) + ? prevState.loaded + : [...prevState.loaded, index], + }); } - return { - current, - previous: prevState.current, - loaded: prevState.loaded.includes(current) - ? prevState.loaded - : [...prevState.loaded, current], - }; + return nextState; } constructor(props) { super(props); - const { routes, index } = this.props.navigationState; + const { index } = this.props.navigationState; this.state = { - tabs: routes.map((_, i) => new Animated.Value(i === index ? 1 : 0)), - shifts: routes.map( - (_, i) => - new Animated.Value(this._calculateShift(index, i, routes.length)) - ), + tabs: [], + shifts: [], index: new Animated.Value(index), ripple: new Animated.Value(MIN_RIPPLE_SCALE), touch: new Animated.Value(MIN_RIPPLE_SCALE), @@ -336,7 +379,7 @@ class BottomNavigation extends React.Component, State> { ), ...routes.map((_, i) => Animated.timing(this.state.shifts[i], { - toValue: this._calculateShift(index, i, routes.length), + toValue: calculateShift(index, i, routes.length), duration: 200, easing: Easing.out(Easing.sin), useNativeDriver: true, @@ -354,33 +397,6 @@ class BottomNavigation extends React.Component, State> { }); } - _calculateShift = (activeIndex, currentIndex, numberOfItems) => { - if (activeIndex < currentIndex) { - // If the new active tab comes before current tab, current tab will shift towards right - return 2 * MIN_SHIFT_AMOUNT; - } - - if (activeIndex > currentIndex) { - // If the new active tab comes after current tab, current tab will shift towards left - return -2 * MIN_SHIFT_AMOUNT; - } - - if (activeIndex === currentIndex) { - if (currentIndex === 0) { - // If the current tab is the new active tab and its the first tab, it'll shift towards right - return MIN_SHIFT_AMOUNT; - } - - if (currentIndex === numberOfItems - 1) { - // If the current tab is the new active tab and its the last tab, it'll shift towards left - return -MIN_SHIFT_AMOUNT; - } - } - - // If the current tab is the new active tab and its somewhere in the middle, it won't shift - return 0; - }; - _handleLayout = e => this.setState({ layout: { @@ -474,6 +490,7 @@ class BottomNavigation extends React.Component, State> { .rgb() .string(); + const touchRippleSize = layout.width / routes.length; const maxTabWidth = routes.length > 3 ? MIN_TAB_WIDTH : MAX_TAB_WIDTH; const tabWidth = Math.min( // Account for horizontal padding around the items @@ -569,14 +586,14 @@ class BottomNavigation extends React.Component, State> { styles.ripple, { // Set top and left values so that the ripple's center is same as the tab's center - top: BAR_HEIGHT / 2 - SMALL_RIPPLE_SIZE / 2, + top: BAR_HEIGHT / 2 - touchRippleSize / 2, left: navigationState.index * tabWidth + tabWidth / 2 - - SMALL_RIPPLE_SIZE / 2, - height: SMALL_RIPPLE_SIZE, - width: SMALL_RIPPLE_SIZE, - borderRadius: SMALL_RIPPLE_SIZE / 2, + touchRippleSize / 2, + height: touchRippleSize, + width: touchRippleSize, + borderRadius: touchRippleSize / 2, backgroundColor: touchColor, transform: [ { diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap index e71b018..af2ae26 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap @@ -88,11 +88,11 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` }, Object { "backgroundColor": "rgba(0, 0, 0, 0.12)", - "borderRadius": 48, - "height": 96, - "left": -54.666666666666664, + "borderRadius": 0, + "height": 0, + "left": -6.666666666666667, "opacity": 0.002, - "top": -20, + "top": 28, "transform": Array [ Object { "translateX": 0, @@ -101,7 +101,7 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` "scale": 0.001, }, ], - "width": 96, + "width": 0, }, ] } @@ -679,11 +679,11 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` }, Object { "backgroundColor": "rgba(255, 255, 255, 0.12)", - "borderRadius": 48, - "height": 96, - "left": -52, + "borderRadius": 0, + "height": 0, + "left": -4, "opacity": 0.002, - "top": -20, + "top": 28, "transform": Array [ Object { "translateX": 10, @@ -692,7 +692,7 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` "scale": 0.001, }, ], - "width": 96, + "width": 0, }, ] } @@ -1330,11 +1330,11 @@ exports[`renders non-shifting bottom navigation 1`] = ` }, Object { "backgroundColor": "rgba(0, 0, 0, 0.12)", - "borderRadius": 48, - "height": 96, - "left": -54.666666666666664, + "borderRadius": 0, + "height": 0, + "left": -6.666666666666667, "opacity": 0.002, - "top": -20, + "top": 28, "transform": Array [ Object { "translateX": 0, @@ -1343,7 +1343,7 @@ exports[`renders non-shifting bottom navigation 1`] = ` "scale": 0.001, }, ], - "width": 96, + "width": 0, }, ] } @@ -2029,11 +2029,11 @@ exports[`renders shifting bottom navigation 1`] = ` }, Object { "backgroundColor": "rgba(255, 255, 255, 0.12)", - "borderRadius": 48, - "height": 96, - "left": -52, + "borderRadius": 0, + "height": 0, + "left": -4, "opacity": 0.002, - "top": -20, + "top": 28, "transform": Array [ Object { "translateX": 10, @@ -2042,7 +2042,7 @@ exports[`renders shifting bottom navigation 1`] = ` "scale": 0.001, }, ], - "width": 96, + "width": 0, }, ] }