1
1
import React from 'react'
2
- import {
3
- LayoutChangeEvent ,
4
- StyleSheet ,
5
- useWindowDimensions ,
6
- View ,
7
- } from 'react-native'
2
+ import { StyleSheet , useWindowDimensions , View } from 'react-native'
8
3
import PagerView from 'react-native-pager-view'
9
4
import Animated , {
10
5
runOnJS ,
@@ -15,6 +10,7 @@ import Animated, {
15
10
useSharedValue ,
16
11
withDelay ,
17
12
withTiming ,
13
+ useFrameCallback ,
18
14
} from 'react-native-reanimated'
19
15
20
16
import { Context , TabNameContext } from './Context'
@@ -27,6 +23,7 @@ import {
27
23
useContainerRef ,
28
24
usePageScrollHandler ,
29
25
useTabProps ,
26
+ useLayoutHeight ,
30
27
} from './hooks'
31
28
import {
32
29
CollapsibleProps ,
@@ -93,25 +90,29 @@ export const Container = React.memo(
93
90
const windowWidth = useWindowDimensions ( ) . width
94
91
const width = customWidth ?? windowWidth
95
92
96
- const containerHeight = useSharedValue < number | undefined > ( undefined )
93
+ const [ containerHeight , getContainerLayoutHeight ] = useLayoutHeight ( )
97
94
98
- const tabBarHeight = useSharedValue < number | undefined > (
99
- initialTabBarHeight
100
- )
95
+ const [ tabBarHeight , getTabBarHeight ] =
96
+ useLayoutHeight ( initialTabBarHeight )
101
97
102
- const headerHeight = useSharedValue < number | undefined > (
98
+ const [ headerHeight , getHeaderHeight ] = useLayoutHeight (
103
99
! renderHeader ? 0 : initialHeaderHeight
104
100
)
101
+ const initialIndex = React . useMemo (
102
+ ( ) =>
103
+ initialTabName
104
+ ? tabNamesArray . findIndex ( ( n ) => n === initialTabName )
105
+ : 0 ,
106
+ [ initialTabName , tabNamesArray ]
107
+ )
105
108
106
- const contentInset = useDerivedValue ( ( ) => {
109
+ const contentInset = React . useMemo ( ( ) => {
107
110
if ( allowHeaderOverscroll ) return 0
108
111
109
112
// necessary for the refresh control on iOS to be positioned underneath the header
110
113
// this also adjusts the scroll bars to clamp underneath the header area
111
- return IS_IOS
112
- ? ( headerHeight . value || 0 ) + ( tabBarHeight . value || 0 )
113
- : 0
114
- } )
114
+ return IS_IOS ? ( headerHeight || 0 ) + ( tabBarHeight || 0 ) : 0
115
+ } , [ headerHeight , tabBarHeight , allowHeaderOverscroll ] )
115
116
116
117
const snappingTo : ContextType [ 'snappingTo' ] = useSharedValue ( 0 )
117
118
const offset : ContextType [ 'offset' ] = useSharedValue ( 0 )
@@ -131,22 +132,16 @@ export const Container = React.memo(
131
132
( ) => tabNamesArray ,
132
133
[ tabNamesArray ]
133
134
)
134
- const index : ContextType [ 'index' ] = useSharedValue (
135
- initialTabName
136
- ? tabNames . value . findIndex ( ( n ) => n === initialTabName )
137
- : 0
138
- )
135
+ const index : ContextType [ 'index' ] = useSharedValue ( initialIndex )
139
136
140
137
const focusedTab : ContextType [ 'focusedTab' ] =
141
138
useDerivedValue < TabName > ( ( ) => {
142
139
return tabNames . value [ index . value ]
143
140
} , [ tabNames ] )
144
- const calculateNextOffset = useSharedValue ( index . value )
141
+ const calculateNextOffset = useSharedValue ( initialIndex )
145
142
const headerScrollDistance : ContextType [ 'headerScrollDistance' ] =
146
143
useDerivedValue ( ( ) => {
147
- return headerHeight . value !== undefined
148
- ? headerHeight . value - minHeaderHeight
149
- : 0
144
+ return headerHeight !== undefined ? headerHeight - minHeaderHeight : 0
150
145
} , [ headerHeight , minHeaderHeight ] )
151
146
152
147
const indexDecimal : ContextType [ 'indexDecimal' ] = useSharedValue (
@@ -167,7 +162,7 @@ export const Container = React.memo(
167
162
scrollToImpl (
168
163
refMap [ name ] ,
169
164
0 ,
170
- scrollYCurrent . value - contentInset . value ,
165
+ scrollYCurrent . value - contentInset ,
171
166
false
172
167
)
173
168
}
@@ -213,6 +208,33 @@ export const Container = React.memo(
213
208
[ onIndexChange , onTabChange ]
214
209
)
215
210
211
+ const syncCurrentTabScrollPosition = ( ) => {
212
+ 'worklet'
213
+
214
+ const name = tabNamesArray [ index . value ]
215
+ scrollToImpl (
216
+ refMap [ name ] ,
217
+ 0 ,
218
+ scrollYCurrent . value - contentInset ,
219
+ false
220
+ )
221
+ }
222
+
223
+ /*
224
+ * We run syncCurrentTabScrollPosition in every frame after the index
225
+ * changes for about 1500ms because the Lists can be late to accept the
226
+ * scrollTo event we send. This fixes the issue of the scroll position
227
+ * jumping when the user changes tab.
228
+ * */
229
+ const toggleSyncScrollFrame = ( toggle : boolean ) =>
230
+ syncScrollFrame . setActive ( toggle )
231
+ const syncScrollFrame = useFrameCallback ( ( { timeSinceFirstFrame } ) => {
232
+ syncCurrentTabScrollPosition ( )
233
+ if ( timeSinceFirstFrame > 1500 ) {
234
+ runOnJS ( toggleSyncScrollFrame ) ( false )
235
+ }
236
+ } , false )
237
+
216
238
useAnimatedReaction (
217
239
( ) => {
218
240
return calculateNextOffset . value
@@ -236,13 +258,14 @@ export const Container = React.memo(
236
258
scrollYCurrent . value =
237
259
scrollY . value [ tabNames . value [ index . value ] ] || 0
238
260
}
261
+ runOnJS ( toggleSyncScrollFrame ) ( true )
239
262
}
240
263
} ,
241
264
[ ]
242
265
)
243
266
244
267
useAnimatedReaction (
245
- ( ) => headerHeight . value ,
268
+ ( ) => headerHeight ,
246
269
( _current , prev ) => {
247
270
if ( prev === undefined ) {
248
271
// sync scroll if we started with undefined header height
@@ -267,32 +290,6 @@ export const Container = React.memo(
267
290
}
268
291
} , [ revealHeaderOnScroll ] )
269
292
270
- const getHeaderHeight = React . useCallback (
271
- ( event : LayoutChangeEvent ) => {
272
- const height = event . nativeEvent . layout . height
273
- if ( headerHeight . value !== height ) {
274
- headerHeight . value = height
275
- }
276
- } ,
277
- [ headerHeight ]
278
- )
279
-
280
- const getTabBarHeight = React . useCallback (
281
- ( event : LayoutChangeEvent ) => {
282
- const height = event . nativeEvent . layout . height
283
- if ( tabBarHeight . value !== height ) tabBarHeight . value = height
284
- } ,
285
- [ tabBarHeight ]
286
- )
287
-
288
- const onLayout = React . useCallback (
289
- ( event : LayoutChangeEvent ) => {
290
- const height = event . nativeEvent . layout . height
291
- if ( containerHeight . value !== height ) containerHeight . value = height
292
- } ,
293
- [ containerHeight ]
294
- )
295
-
296
293
const onTabPress = React . useCallback (
297
294
( name : TabName ) => {
298
295
const i = tabNames . value . findIndex ( ( n ) => n === name )
@@ -302,7 +299,7 @@ export const Container = React.memo(
302
299
runOnUI ( scrollToImpl ) (
303
300
ref ,
304
301
0 ,
305
- headerScrollDistance . value - contentInset . value ,
302
+ headerScrollDistance . value - contentInset ,
306
303
true
307
304
)
308
305
} else {
@@ -313,11 +310,14 @@ export const Container = React.memo(
313
310
[ containerRef , refMap , contentInset ]
314
311
)
315
312
316
- React . useEffect ( ( ) => {
317
- if ( index . value >= tabNamesArray . length ) {
318
- onTabPress ( tabNamesArray [ tabNamesArray . length - 1 ] )
313
+ useAnimatedReaction (
314
+ ( ) => tabNamesArray . length ,
315
+ ( tabLength ) => {
316
+ if ( index . value >= tabLength ) {
317
+ runOnJS ( onTabPress ) ( tabNamesArray [ tabLength - 1 ] )
318
+ }
319
319
}
320
- } , [ index . value , onTabPress , tabNamesArray ] )
320
+ )
321
321
322
322
const pageScrollHandler = usePageScrollHandler ( {
323
323
onPageScroll : ( e ) => {
@@ -381,7 +381,7 @@ export const Container = React.memo(
381
381
>
382
382
< Animated . View
383
383
style = { [ styles . container , { width } , containerStyle ] }
384
- onLayout = { onLayout }
384
+ onLayout = { getContainerLayoutHeight }
385
385
pointerEvents = "box-none"
386
386
>
387
387
< Animated . View
@@ -430,7 +430,7 @@ export const Container = React.memo(
430
430
< AnimatedPagerView
431
431
ref = { containerRef }
432
432
onPageScroll = { pageScrollHandler }
433
- initialPage = { index . value }
433
+ initialPage = { initialIndex }
434
434
{ ...pagerProps }
435
435
style = { [ pagerProps ?. style , StyleSheet . absoluteFill ] }
436
436
>
0 commit comments