Skip to content

Commit 5748ff5

Browse files
committed
feat: save memory usage for inactive routes in bottom navigation
1 parent bb845c0 commit 5748ff5

File tree

2 files changed

+102
-13
lines changed

2 files changed

+102
-13
lines changed

src/components/BottomNavigation.js

+50-5
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ type State = {
168168
* The amount of horizontal shift for each tab item.
169169
*/
170170
shifts: Animated.Value[],
171+
/**
172+
* The top offset for each tab item to position it offscreen.
173+
* Placing items offscreen helps to save memory usage for inactive screens with removeClippedSubviews.
174+
* We use animated values for this to prevent unnecesary re-renders.
175+
*/
176+
offsets: Animated.Value[],
171177
/**
172178
* Index of the currently active tab. Used for setting the background color.
173179
* Use don't use the color as an animated value directly, because `setValue` seems to be buggy with colors.
@@ -206,6 +212,7 @@ const MAX_TAB_WIDTH = 168;
206212
const BAR_HEIGHT = 56;
207213
const ACTIVE_LABEL_SIZE = 14;
208214
const INACTIVE_LABEL_SIZE = 12;
215+
const FAR_FAR_AWAY = 9999;
209216

210217
const calculateShift = (activeIndex, currentIndex, numberOfItems) => {
211218
if (activeIndex < currentIndex) {
@@ -311,10 +318,15 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
311318
prevState.shifts[i] ||
312319
new Animated.Value(calculateShift(index, i, routes.length))
313320
);
321+
const offsets = routes.map(
322+
// offscreen === 1, normal === 0
323+
(_, i) => prevState.offsets[i] || new Animated.Value(i === index ? 0 : 1)
324+
);
314325

315326
const nextState = {
316327
tabs,
317328
shifts,
329+
offsets,
318330
};
319331

320332
if (index !== prevState.current) {
@@ -341,6 +353,7 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
341353
this.state = {
342354
tabs: [],
343355
shifts: [],
356+
offsets: [],
344357
index: new Animated.Value(index),
345358
ripple: new Animated.Value(MIN_RIPPLE_SCALE),
346359
touch: new Animated.Value(MIN_RIPPLE_SCALE),
@@ -358,6 +371,13 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
358371

359372
const { routes, index } = this.props.navigationState;
360373

374+
// Reset offsets of previous and current tabs before animation
375+
this.state.offsets.forEach((offset, i) => {
376+
if (i === index || i === prevProps.navigationState.index) {
377+
offset.setValue(0);
378+
}
379+
});
380+
361381
// Reset the ripple to avoid glitch if it's currently animating
362382
this.state.ripple.setValue(MIN_RIPPLE_SCALE);
363383

@@ -387,13 +407,25 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
387407
),
388408
]),
389409
]),
390-
]).start(() => {
410+
]).start(({ finished }) => {
391411
// Workaround a bug in native animations where this is reset after first animation
392412
this.state.tabs.map((tab, i) => tab.setValue(i === index ? 1 : 0));
393413

394414
// Update the index to change bar's bacground color and then hide the ripple
395415
this.state.index.setValue(index);
396416
this.state.ripple.setValue(MIN_RIPPLE_SCALE);
417+
418+
if (finished) {
419+
// Position all inactive screens offscreen to save memory usage
420+
// Only do it when animation has finished to avoid glitches mid-transition if switching fast
421+
this.state.offsets.forEach((offset, i) => {
422+
if (i === index) {
423+
offset.setValue(0);
424+
} else {
425+
offset.setValue(1);
426+
}
427+
});
428+
}
397429
});
398430
}
399431

@@ -522,6 +554,11 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
522554
})
523555
: 0;
524556

557+
const top = this.state.offsets[index].interpolate({
558+
inputRange: [0, 1],
559+
outputRange: [0, FAR_FAR_AWAY],
560+
});
561+
525562
return (
526563
<Animated.View
527564
key={route.key}
@@ -532,11 +569,19 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
532569
StyleSheet.absoluteFill,
533570
{ opacity, transform: [{ translateY }] },
534571
]}
572+
collapsable={false}
573+
removeClippedSubviews={
574+
// On iOS, set removeClippedSubviews to true only when not focused
575+
// This is an workaround for a bug where the clipped view never re-appears
576+
Platform.OS === 'ios' ? navigationState.index !== index : true
577+
}
535578
>
536-
{renderScene({
537-
route,
538-
jumpTo: this._jumpTo,
539-
})}
579+
<Animated.View style={[styles.content, { top }]}>
580+
{renderScene({
581+
route,
582+
jumpTo: this._jumpTo,
583+
})}
584+
</Animated.View>
540585
</Animated.View>
541586
);
542587
})}

src/components/__tests__/__snapshots__/BottomNavigation.test.js.snap

+52-8
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
2626
}
2727
>
2828
<View
29-
collapsable={undefined}
29+
collapsable={false}
3030
pointerEvents="auto"
31+
removeClippedSubviews={false}
3132
style={
3233
Object {
3334
"bottom": 0,
@@ -44,7 +45,17 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = `
4445
}
4546
}
4647
>
47-
Route: 0
48+
<View
49+
collapsable={undefined}
50+
style={
51+
Object {
52+
"flex": 1,
53+
"top": 0,
54+
}
55+
}
56+
>
57+
Route: 0
58+
</View>
4859
</View>
4960
</View>
5061
<View
@@ -560,8 +571,9 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
560571
}
561572
>
562573
<View
563-
collapsable={undefined}
574+
collapsable={false}
564575
pointerEvents="auto"
576+
removeClippedSubviews={false}
565577
style={
566578
Object {
567579
"bottom": 0,
@@ -578,7 +590,17 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = `
578590
}
579591
}
580592
>
581-
Route: 0
593+
<View
594+
collapsable={undefined}
595+
style={
596+
Object {
597+
"flex": 1,
598+
"top": 0,
599+
}
600+
}
601+
>
602+
Route: 0
603+
</View>
582604
</View>
583605
</View>
584606
<View
@@ -1211,8 +1233,9 @@ exports[`renders non-shifting bottom navigation 1`] = `
12111233
}
12121234
>
12131235
<View
1214-
collapsable={undefined}
1236+
collapsable={false}
12151237
pointerEvents="auto"
1238+
removeClippedSubviews={false}
12161239
style={
12171240
Object {
12181241
"bottom": 0,
@@ -1229,7 +1252,17 @@ exports[`renders non-shifting bottom navigation 1`] = `
12291252
}
12301253
}
12311254
>
1232-
Route: 0
1255+
<View
1256+
collapsable={undefined}
1257+
style={
1258+
Object {
1259+
"flex": 1,
1260+
"top": 0,
1261+
}
1262+
}
1263+
>
1264+
Route: 0
1265+
</View>
12331266
</View>
12341267
</View>
12351268
<View
@@ -2093,8 +2126,9 @@ exports[`renders shifting bottom navigation 1`] = `
20932126
}
20942127
>
20952128
<View
2096-
collapsable={undefined}
2129+
collapsable={false}
20972130
pointerEvents="auto"
2131+
removeClippedSubviews={false}
20982132
style={
20992133
Object {
21002134
"bottom": 0,
@@ -2111,7 +2145,17 @@ exports[`renders shifting bottom navigation 1`] = `
21112145
}
21122146
}
21132147
>
2114-
Route: 0
2148+
<View
2149+
collapsable={undefined}
2150+
style={
2151+
Object {
2152+
"flex": 1,
2153+
"top": 0,
2154+
}
2155+
}
2156+
>
2157+
Route: 0
2158+
</View>
21152159
</View>
21162160
</View>
21172161
<View

0 commit comments

Comments
 (0)