fix: support dynamically changing routes in bottom navigation

This commit is contained in:
Satyajit Sahoo
2018-04-12 12:13:32 +02:00
parent fa352f3148
commit 5e38f9ccd8
2 changed files with 87 additions and 70 deletions

View File

@@ -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<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))
);
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<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,
@@ -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: {
@@ -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
@@ -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: [
{

View File

@@ -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,
},
]
}