@@ -168,6 +168,12 @@ type State = {
168
168
* The amount of horizontal shift for each tab item.
169
169
*/
170
170
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 [ ] ,
171
177
/**
172
178
* Index of the currently active tab. Used for setting the background color.
173
179
* 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;
206
212
const BAR_HEIGHT = 56 ;
207
213
const ACTIVE_LABEL_SIZE = 14 ;
208
214
const INACTIVE_LABEL_SIZE = 12 ;
215
+ const FAR_FAR_AWAY = 9999 ;
209
216
210
217
const calculateShift = ( activeIndex , currentIndex , numberOfItems ) => {
211
218
if ( activeIndex < currentIndex ) {
@@ -311,10 +318,15 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
311
318
prevState . shifts [ i ] ||
312
319
new Animated . Value ( calculateShift ( index , i , routes . length ) )
313
320
) ;
321
+ const offsets = routes . map (
322
+ // offscreen === 1, normal === 0
323
+ ( _ , i ) => prevState . offsets [ i ] || new Animated . Value ( i === index ? 0 : 1 )
324
+ ) ;
314
325
315
326
const nextState = {
316
327
tabs ,
317
328
shifts ,
329
+ offsets ,
318
330
} ;
319
331
320
332
if (index !== prevState.current) {
@@ -341,6 +353,7 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
341
353
this . state = {
342
354
tabs : [ ] ,
343
355
shifts : [ ] ,
356
+ offsets : [ ] ,
344
357
index : new Animated . Value ( index ) ,
345
358
ripple : new Animated . Value ( MIN_RIPPLE_SCALE ) ,
346
359
touch : new Animated . Value ( MIN_RIPPLE_SCALE ) ,
@@ -358,6 +371,13 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
358
371
359
372
const { routes , index } = this.props.navigationState;
360
373
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
+
361
381
// Reset the ripple to avoid glitch if it's currently animating
362
382
this . state . ripple . setValue ( MIN_RIPPLE_SCALE ) ;
363
383
@@ -387,13 +407,25 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
387
407
),
388
408
]),
389
409
]),
390
- ]).start(() => {
410
+ ]).start(({ finished } ) => {
391
411
// Workaround a bug in native animations where this is reset after first animation
392
412
this . state . tabs . map ( ( tab , i ) => tab . setValue ( i === index ? 1 : 0 ) ) ;
393
413
394
414
// Update the index to change bar's bacground color and then hide the ripple
395
415
this . state . index . setValue ( index ) ;
396
416
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
+ }
397
429
} ) ;
398
430
}
399
431
@@ -522,6 +554,11 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
522
554
} )
523
555
: 0 ;
524
556
557
+ const top = this . state . offsets [ index ] . interpolate ( {
558
+ inputRange : [ 0 , 1 ] ,
559
+ outputRange : [ 0 , FAR_FAR_AWAY ] ,
560
+ } ) ;
561
+
525
562
return (
526
563
< Animated . View
527
564
key = { route . key }
@@ -532,11 +569,19 @@ class BottomNavigation<T: *> extends React.Component<Props<T>, State> {
532
569
StyleSheet . absoluteFill ,
533
570
{ opacity, transform : [ { translateY } ] } ,
534
571
] }
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
+ }
535
578
>
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 >
540
585
</ Animated . View >
541
586
) ;
542
587
} ) }
0 commit comments