Skip to content

Commit

Permalink
fix: support dynamically changing routes in bottom navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Apr 12, 2018
1 parent fa352f3 commit 5e38f9c
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 70 deletions.
117 changes: 67 additions & 50 deletions src/components/BottomNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -272,32 +298,49 @@ class BottomNavigation<T: Route> extends React.Component<Props<T>, 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))
);

return {
current,
previous: prevState.current,
loaded: prevState.loaded.includes(current)
? prevState.loaded
: [...prevState.loaded, current],
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 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),
Expand Down Expand Up @@ -336,7 +379,7 @@ class BottomNavigation<T: Route> extends React.Component<Props<T>, 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,
Expand All @@ -354,33 +397,6 @@ class BottomNavigation<T: Route> extends React.Component<Props<T>, 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: {
Expand Down Expand Up @@ -474,6 +490,7 @@ class BottomNavigation<T: Route> extends React.Component<Props<T>, 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
Expand Down Expand Up @@ -569,14 +586,14 @@ class BottomNavigation<T: Route> extends React.Component<Props<T>, 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: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -101,7 +101,7 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
"scale": 0.001,
},
],
"width": 96,
"width": 0,
},
]
}
Expand Down Expand Up @@ -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,
Expand All @@ -692,7 +692,7 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
"scale": 0.001,
},
],
"width": 96,
"width": 0,
},
]
}
Expand Down Expand Up @@ -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,
Expand All @@ -1343,7 +1343,7 @@ exports[`renders non-shifting bottom navigation 1`] = `
"scale": 0.001,
},
],
"width": 96,
"width": 0,
},
]
}
Expand Down Expand Up @@ -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,
Expand All @@ -2042,7 +2042,7 @@ exports[`renders shifting bottom navigation 1`] = `
"scale": 0.001,
},
],
"width": 96,
"width": 0,
},
]
}
Expand Down

0 comments on commit 5e38f9c

Please sign in to comment.