1
1
// TODO: support commented props
2
2
// TODO: disable scroll when content width is shorter than screen width
3
- import React , { useEffect , useState , useCallback , useMemo , useRef , useContext } from 'react' ;
4
- import {
5
- StyleSheet ,
6
- ScrollView ,
7
- Platform ,
8
- TextProps ,
9
- StyleProp ,
10
- ViewStyle ,
11
- LayoutRectangle ,
12
- NativeSyntheticEvent ,
13
- NativeScrollEvent
14
- } from 'react-native' ;
3
+ import React , { useEffect , useMemo , useRef , useContext , ReactNode } from 'react' ;
4
+ import { StyleSheet , ScrollView , Platform , TextProps , StyleProp , ViewStyle } from 'react-native' ;
15
5
import Reanimated from 'react-native-reanimated' ;
16
6
import _ from 'lodash' ;
17
7
@@ -24,6 +14,8 @@ import {Constants} from '../../helpers';
24
14
import { LogService } from '../../services' ;
25
15
import FadedScrollView from './FadedScrollView' ;
26
16
17
+ import focusItemsHelper , { OffsetType } from '../../helpers/FocusItemHelper' ;
18
+
27
19
const { Code, Value, interpolate, block, set} = Reanimated ;
28
20
29
21
const DEFAULT_HEIGHT = 48 ;
@@ -150,15 +142,15 @@ const TabBar = (props: Props) => {
150
142
activeBackgroundColor,
151
143
backgroundColor,
152
144
containerWidth : propsContainerWidth ,
153
- centerSelected : propsCenterSelected ,
145
+ centerSelected,
154
146
containerStyle,
155
147
testID,
156
148
children : propsChildren
157
149
} = props ;
158
150
159
151
const context = useContext ( TabBarContext ) ;
160
152
// @ts -ignore // TODO: typescript
161
- const { itemStates, items : contextItems , selectedIndex , currentPage, targetPage, registerTabItems} = context ;
153
+ const { itemStates, items : contextItems , currentPage, targetPage, registerTabItems, selectedIndex } = context ;
162
154
163
155
const children = useRef < Props [ 'children' ] > ( _ . filter ( propsChildren , ( child : ChildProps ) => ! ! child ) ) ;
164
156
@@ -197,12 +189,7 @@ const TabBar = (props: Props) => {
197
189
}
198
190
} , [ ] ) ;
199
191
200
- const [ itemsWidths , setItemsWidths ] = useState < number [ ] > ( [ ] ) ;
201
- const [ itemsOffsets , setItemsOffsets ] = useState < number [ ] > ( [ ] ) ;
202
- const [ scrollEnabled , setScrollEnabled ] = useState < boolean > ( false ) ;
203
192
const tabBar = useRef < ScrollView > ( null ) ;
204
- const tabBarScrollOffset = useRef < number > ( 0 ) ;
205
- const contentWidth = useRef < number > ( 0 ) ;
206
193
207
194
const containerWidth : number = useMemo ( ( ) => {
208
195
return propsContainerWidth || Constants . screenWidth ;
@@ -212,136 +199,101 @@ const TabBar = (props: Props) => {
212
199
} , [ contextItems , propsItems ] ) ;
213
200
214
201
const itemsCount = useRef < number > ( items ? _ . size ( items ) : React . Children . count ( children . current ) ) ;
215
- const _itemsWidths = useRef < ( number | null | undefined ) [ ] > ( _ . times ( itemsCount . current , ( ) => null ) ) ;
216
- const _itemsOffsets = useRef < ( number | null | undefined ) [ ] > ( _ . times ( itemsCount . current , ( ) => null ) ) ;
217
-
218
- const onScroll = useCallback ( ( { nativeEvent : { contentOffset} } : NativeSyntheticEvent < NativeScrollEvent > ) => {
219
- tabBarScrollOffset . current = contentOffset . x ;
220
- if ( Constants . isRTL && Constants . isAndroid ) {
221
- const scrollingWidth = Math . max ( 0 , contentWidth . current - containerWidth ) ;
222
- tabBarScrollOffset . current = scrollingWidth - tabBarScrollOffset . current ;
223
- }
224
- } ,
225
- [ containerWidth ] ) ;
226
-
227
- const snapBreakpoints = useMemo ( ( ) => {
228
- return itemsWidths && itemsOffsets && itemsWidths . length > 0 && itemsOffsets . length > 0 && propsCenterSelected
229
- ? _ . times ( itemsWidths . length , index => {
230
- const screenCenter = containerWidth / 2 ;
231
- const itemOffset = itemsOffsets [ index ] ;
232
- const itemWidth = itemsWidths [ index ] ;
233
- return itemOffset - screenCenter + itemWidth / 2 ;
234
- } )
235
- : undefined ;
236
- } , [ itemsWidths , itemsOffsets , propsCenterSelected , containerWidth ] ) ;
237
-
238
- const guesstimateCenterValue = 60 ;
239
- const centerOffset = propsCenterSelected ? containerWidth / 2 - guesstimateCenterValue : 0 ;
240
-
241
- // TODO: move this logic into a ScrollPresenter or something
242
- const focusSelected = useCallback ( ( [ index ] : readonly number [ ] , animated = true ) => {
243
- const itemOffset = _itemsOffsets . current [ index ] ;
244
- const itemWidth = _itemsWidths . current [ index ] ;
245
- const screenCenter = containerWidth / 2 ;
246
-
247
- let targetOffset ;
248
-
249
- if ( ! _ . isNil ( itemOffset ) && ! _ . isNil ( itemWidth ) ) {
250
- if ( propsCenterSelected ) {
251
- targetOffset = itemOffset - screenCenter + itemWidth / 2 ;
252
- } else if ( itemOffset < tabBarScrollOffset . current ) {
253
- targetOffset = itemOffset - itemWidth ;
254
- } else if ( itemOffset + itemWidth > tabBarScrollOffset . current + containerWidth ) {
255
- const offsetChange = Math . max ( 0 , itemOffset - ( tabBarScrollOffset . current + containerWidth ) ) ;
256
- targetOffset = tabBarScrollOffset . current + offsetChange + itemWidth ;
257
- }
258
202
259
- if ( ! _ . isUndefined ( targetOffset ) ) {
260
- if ( Constants . isRTL && Constants . isAndroid ) {
261
- const scrollingWidth = Math . max ( 0 , contentWidth . current - containerWidth ) ;
262
- targetOffset = scrollingWidth - targetOffset ;
263
- }
203
+ const { onItemLayout, itemsWidths, focusIndex} = focusItemsHelper ( {
204
+ scrollViewRef : tabBar ,
205
+ itemsCount : itemsCount . current ,
206
+ selectedIndex,
207
+ offsetType : centerSelected ? OffsetType . CENTER : OffsetType . DYNAMIC
208
+ } ) ;
264
209
265
- if ( tabBar ?. current ) {
266
- tabBar . current . scrollTo ( { x : targetOffset , animated} ) ;
267
- }
268
- }
210
+ const indicatorOffsets = useMemo ( ( ) : number [ ] => {
211
+ let index = 0 ;
212
+ const offsets = [ ] ;
213
+ offsets . push ( 0 ) ;
214
+ while ( index < itemsWidths . length - 1 ) {
215
+ ++ index ;
216
+ offsets [ index ] = offsets [ index - 1 ] + itemsWidths [ index - 1 ] ;
269
217
}
270
- } ,
271
- [ containerWidth ] ) ;
272
-
273
- function getItemsOffsets ( ) {
274
- return _ . times ( _itemsWidths . current . length ,
275
- i => _ . chain ( _itemsWidths . current ) . take ( i ) . sum ( ) . value ( ) + centerOffset ) ;
276
- }
277
218
278
- const setItemsLayouts = useCallback ( ( ) => {
279
- // It's important to calculate itemOffsets for RTL support
280
- _itemsOffsets . current = getItemsOffsets ( ) ;
281
- const itemsOffsets = _ . map ( _itemsOffsets . current , offset => ( offset ? offset : 0 ) + INDICATOR_INSET ) ;
282
- const itemsWidths = _ . map ( _itemsWidths . current , width => ( width ? width : 0 ) - INDICATOR_INSET * 2 ) ;
283
- contentWidth . current = _ . sum ( _itemsWidths . current ) ;
284
- const scrollEnabled = contentWidth . current > containerWidth ;
285
-
286
- setItemsWidths ( itemsWidths ) ;
287
- setItemsOffsets ( itemsOffsets ) ;
288
- setScrollEnabled ( scrollEnabled ) ;
289
- focusSelected ( [ selectedIndex ] , false ) ;
290
- } , [ containerWidth , selectedIndex ] ) ;
291
-
292
- const onItemLayout = useCallback ( ( { width} : Partial < LayoutRectangle > , itemIndex : number ) => {
293
- _itemsWidths . current [ itemIndex ] = width ;
294
- if ( ! _ . includes ( _itemsWidths . current , null ) ) {
295
- setItemsLayouts ( ) ;
296
- }
297
- } ,
298
- [ setItemsLayouts ] ) ;
299
-
300
- const _renderTabBarItems = _ . map ( items , ( item , index ) => {
301
- return (
302
- < TabBarItem
303
- labelColor = { labelColor }
304
- selectedLabelColor = { selectedLabelColor }
305
- labelStyle = { labelStyle }
306
- selectedLabelStyle = { selectedLabelStyle }
307
- uppercase = { uppercase }
308
- iconColor = { iconColor }
309
- selectedIconColor = { selectedIconColor }
310
- activeBackgroundColor = { activeBackgroundColor }
311
- key = { item . label }
312
- // width={_itemsWidths.current[index]}
313
- { ...item }
314
- { ...context }
315
- index = { index }
316
- state = { itemStates [ index ] }
317
- onLayout = { onItemLayout }
318
- />
319
- ) ;
320
- } ) ;
219
+ return offsets ;
220
+ } , [ itemsWidths ] ) ;
221
+
222
+ const _renderTabBarItems = useMemo ( ( ) : ReactNode => {
223
+ return _ . map ( items , ( item , index ) => {
224
+ return (
225
+ < TabBarItem
226
+ labelColor = { labelColor }
227
+ selectedLabelColor = { selectedLabelColor }
228
+ labelStyle = { labelStyle }
229
+ selectedLabelStyle = { selectedLabelStyle }
230
+ uppercase = { uppercase }
231
+ iconColor = { iconColor }
232
+ selectedIconColor = { selectedIconColor }
233
+ activeBackgroundColor = { activeBackgroundColor }
234
+ key = { item . label }
235
+ // width={_itemsWidths.current[index]}
236
+ { ...item }
237
+ { ...context }
238
+ index = { index }
239
+ state = { itemStates [ index ] }
240
+ onLayout = { onItemLayout }
241
+ />
242
+ ) ;
243
+ } ) ;
244
+ } , [
245
+ labelColor ,
246
+ selectedLabelColor ,
247
+ labelStyle ,
248
+ selectedLabelStyle ,
249
+ uppercase ,
250
+ iconColor ,
251
+ selectedIconColor ,
252
+ activeBackgroundColor ,
253
+ itemStates ,
254
+ centerSelected ,
255
+ onItemLayout
256
+ ] ) ;
321
257
322
258
// TODO: Remove once props.children is deprecated
323
- const _renderTabBarItemsFromChildren = ! children . current
324
- ? null
325
- : React . Children . map ( children . current , ( child : Partial < ChildProps > , index : number ) => {
326
- // @ts -ignore TODO: typescript - not sure if this can be easily solved
327
- return React . cloneElement ( child , {
328
- labelColor,
329
- selectedLabelColor,
330
- labelStyle,
331
- selectedLabelStyle,
332
- uppercase,
333
- iconColor,
334
- selectedIconColor,
335
- activeBackgroundColor,
336
- ...child . props ,
337
- ...context ,
338
- index,
339
- state : itemStates [ index ] ,
340
- onLayout : onItemLayout
259
+ const _renderTabBarItemsFromChildren = useMemo ( ( ) : ReactNode | null => {
260
+ return ! children . current
261
+ ? null
262
+ : React . Children . map ( children . current , ( child : Partial < ChildProps > , index : number ) => {
263
+ // @ts -ignore TODO: typescript - not sure if this can be easily solved
264
+ return React . cloneElement ( child , {
265
+ labelColor,
266
+ selectedLabelColor,
267
+ labelStyle,
268
+ selectedLabelStyle,
269
+ uppercase,
270
+ iconColor,
271
+ selectedIconColor,
272
+ activeBackgroundColor,
273
+ ...child . props ,
274
+ ...context ,
275
+ index,
276
+ state : itemStates [ index ] ,
277
+ onLayout : centerSelected ? onItemLayout : undefined
278
+ } ) ;
341
279
} ) ;
342
- } ) ;
280
+ } , [
281
+ labelColor ,
282
+ selectedLabelColor ,
283
+ labelStyle ,
284
+ selectedLabelStyle ,
285
+ uppercase ,
286
+ iconColor ,
287
+ selectedIconColor ,
288
+ activeBackgroundColor ,
289
+ itemStates ,
290
+ centerSelected ,
291
+ onItemLayout
292
+ ] ) ;
343
293
344
- const renderTabBarItems = _ . isEmpty ( itemStates ) ? null : items ? _renderTabBarItems : _renderTabBarItemsFromChildren ;
294
+ const renderTabBarItems = useMemo ( ( ) => {
295
+ return _ . isEmpty ( itemStates ) ? null : items ? _renderTabBarItems : _renderTabBarItemsFromChildren ;
296
+ } , [ itemStates , items , _renderTabBarItems , _renderTabBarItemsFromChildren ] ) ;
345
297
346
298
const _indicatorWidth = new Value ( 0 ) ; // TODO: typescript?
347
299
const _indicatorOffset = new Value ( 0 ) ; // TODO: typescript?
@@ -357,44 +309,54 @@ const TabBar = (props: Props) => {
357
309
< Reanimated . View style = { [ styles . selectedIndicator , indicatorStyle , _indicatorTransitionStyle ] } />
358
310
) : undefined ;
359
311
360
- const renderCodeBlock = ( ) => {
312
+ const renderCodeBlock = _ . memoize ( ( ) => {
361
313
const nodes : any [ ] = [ ] ;
362
314
363
315
nodes . push ( set ( _indicatorOffset ,
316
+ interpolate ( currentPage , {
317
+ inputRange : indicatorOffsets . map ( ( _v , i ) => i ) ,
318
+ outputRange : indicatorOffsets
319
+ } ) ) ) ;
320
+ nodes . push ( set ( _indicatorWidth ,
364
321
interpolate ( currentPage , {
365
322
inputRange : itemsWidths . map ( ( _v , i ) => i ) ,
366
323
outputRange : itemsWidths . map ( v => v - 2 * INDICATOR_INSET )
367
324
} ) ) ) ;
368
- nodes . push ( set ( _indicatorWidth ,
369
- interpolate ( currentPage , { inputRange : itemsWidths . map ( ( _v , i ) => i ) , outputRange : itemsWidths } ) ) ) ;
370
325
371
- nodes . push ( Reanimated . onChange ( targetPage , Reanimated . call ( [ targetPage ] , focusSelected ) ) ) ;
326
+ nodes . push ( Reanimated . onChange ( targetPage , Reanimated . call ( [ targetPage ] , focusIndex as any ) ) ) ;
372
327
373
- return < Code > { ( ) => block ( nodes ) } </ Code > ;
374
- } ;
328
+ const temp = < Code > { ( ) => block ( nodes ) } </ Code > ;
329
+ return temp ;
330
+ } ) ;
331
+
332
+ const shadowStyle = useMemo ( ( ) => {
333
+ return enableShadow ? propsShadowStyle || styles . containerShadow : undefined ;
334
+ } , [ enableShadow , propsShadowStyle ] ) ;
335
+
336
+ const _containerStyle = useMemo ( ( ) => {
337
+ return [ styles . container , shadowStyle , { width : containerWidth } , containerStyle ] ;
338
+ } , [ shadowStyle , containerWidth , containerStyle ] ) ;
339
+
340
+ const indicatorContainerStyle = useMemo ( ( ) => {
341
+ return [ styles . tabBar , ! _ . isUndefined ( height ) && { height} , { backgroundColor} ] ;
342
+ } , [ height , backgroundColor ] ) ;
343
+
344
+ const scrollViewContainerStyle = useMemo ( ( ) => {
345
+ return { minWidth : containerWidth } ;
346
+ } , [ containerWidth ] ) ;
375
347
376
- const shadowStyle = enableShadow ? propsShadowStyle || styles . containerShadow : undefined ;
377
348
return (
378
- < View style = { [ styles . container , shadowStyle , { width : containerWidth } , containerStyle ] } >
349
+ < View style = { _containerStyle } >
379
350
< FadedScrollView
380
- // @ts -ignore TODO: typescript
351
+ /**
352
+ // @ts -ignore TODO: typescript */
381
353
ref = { tabBar }
382
354
horizontal
383
- contentContainerStyle = { { minWidth : containerWidth } }
384
- scrollEnabled = { scrollEnabled }
385
- onScroll = { onScroll }
355
+ contentContainerStyle = { scrollViewContainerStyle }
356
+ scrollEnabled // TODO:
386
357
testID = { testID }
387
- snapToOffsets = { snapBreakpoints }
388
358
>
389
- < View
390
- style = { [
391
- styles . tabBar ,
392
- ! _ . isUndefined ( height ) && { height} ,
393
- { paddingHorizontal : centerOffset , backgroundColor}
394
- ] }
395
- >
396
- { renderTabBarItems }
397
- </ View >
359
+ < View style = { indicatorContainerStyle } > { renderTabBarItems } </ View >
398
360
{ renderSelectedIndicator }
399
361
</ FadedScrollView >
400
362
{ _ . size ( itemsWidths ) > 1 && renderCodeBlock ( ) }
0 commit comments