11import 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'
83import PagerView from 'react-native-pager-view'
94import Animated , {
105 runOnJS ,
@@ -15,6 +10,7 @@ import Animated, {
1510 useSharedValue ,
1611 withDelay ,
1712 withTiming ,
13+ useFrameCallback ,
1814} from 'react-native-reanimated'
1915
2016import { Context , TabNameContext } from './Context'
@@ -27,6 +23,7 @@ import {
2723 useContainerRef ,
2824 usePageScrollHandler ,
2925 useTabProps ,
26+ useLayoutHeight ,
3027} from './hooks'
3128import {
3229 CollapsibleProps ,
@@ -93,25 +90,29 @@ export const Container = React.memo(
9390 const windowWidth = useWindowDimensions ( ) . width
9491 const width = customWidth ?? windowWidth
9592
96- const containerHeight = useSharedValue < number | undefined > ( undefined )
93+ const [ containerHeight , getContainerLayoutHeight ] = useLayoutHeight ( )
9794
98- const tabBarHeight = useSharedValue < number | undefined > (
99- initialTabBarHeight
100- )
95+ const [ tabBarHeight , getTabBarHeight ] =
96+ useLayoutHeight ( initialTabBarHeight )
10197
102- const headerHeight = useSharedValue < number | undefined > (
98+ const [ headerHeight , getHeaderHeight ] = useLayoutHeight (
10399 ! renderHeader ? 0 : initialHeaderHeight
104100 )
101+ const initialIndex = React . useMemo (
102+ ( ) =>
103+ initialTabName
104+ ? tabNamesArray . findIndex ( ( n ) => n === initialTabName )
105+ : 0 ,
106+ [ initialTabName , tabNamesArray ]
107+ )
105108
106- const contentInset = useDerivedValue ( ( ) => {
109+ const contentInset = React . useMemo ( ( ) => {
107110 if ( allowHeaderOverscroll ) return 0
108111
109112 // necessary for the refresh control on iOS to be positioned underneath the header
110113 // 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 ] )
115116
116117 const snappingTo : ContextType [ 'snappingTo' ] = useSharedValue ( 0 )
117118 const offset : ContextType [ 'offset' ] = useSharedValue ( 0 )
@@ -131,22 +132,16 @@ export const Container = React.memo(
131132 ( ) => tabNamesArray ,
132133 [ tabNamesArray ]
133134 )
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 )
139136
140137 const focusedTab : ContextType [ 'focusedTab' ] =
141138 useDerivedValue < TabName > ( ( ) => {
142139 return tabNames . value [ index . value ]
143140 } , [ tabNames ] )
144- const calculateNextOffset = useSharedValue ( index . value )
141+ const calculateNextOffset = useSharedValue ( initialIndex )
145142 const headerScrollDistance : ContextType [ 'headerScrollDistance' ] =
146143 useDerivedValue ( ( ) => {
147- return headerHeight . value !== undefined
148- ? headerHeight . value - minHeaderHeight
149- : 0
144+ return headerHeight !== undefined ? headerHeight - minHeaderHeight : 0
150145 } , [ headerHeight , minHeaderHeight ] )
151146
152147 const indexDecimal : ContextType [ 'indexDecimal' ] = useSharedValue (
@@ -167,7 +162,7 @@ export const Container = React.memo(
167162 scrollToImpl (
168163 refMap [ name ] ,
169164 0 ,
170- scrollYCurrent . value - contentInset . value ,
165+ scrollYCurrent . value - contentInset ,
171166 false
172167 )
173168 }
@@ -213,6 +208,33 @@ export const Container = React.memo(
213208 [ onIndexChange , onTabChange ]
214209 )
215210
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+
216238 useAnimatedReaction (
217239 ( ) => {
218240 return calculateNextOffset . value
@@ -236,13 +258,14 @@ export const Container = React.memo(
236258 scrollYCurrent . value =
237259 scrollY . value [ tabNames . value [ index . value ] ] || 0
238260 }
261+ runOnJS ( toggleSyncScrollFrame ) ( true )
239262 }
240263 } ,
241264 [ ]
242265 )
243266
244267 useAnimatedReaction (
245- ( ) => headerHeight . value ,
268+ ( ) => headerHeight ,
246269 ( _current , prev ) => {
247270 if ( prev === undefined ) {
248271 // sync scroll if we started with undefined header height
@@ -267,32 +290,6 @@ export const Container = React.memo(
267290 }
268291 } , [ revealHeaderOnScroll ] )
269292
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-
296293 const onTabPress = React . useCallback (
297294 ( name : TabName ) => {
298295 const i = tabNames . value . findIndex ( ( n ) => n === name )
@@ -302,7 +299,7 @@ export const Container = React.memo(
302299 runOnUI ( scrollToImpl ) (
303300 ref ,
304301 0 ,
305- headerScrollDistance . value - contentInset . value ,
302+ headerScrollDistance . value - contentInset ,
306303 true
307304 )
308305 } else {
@@ -313,11 +310,14 @@ export const Container = React.memo(
313310 [ containerRef , refMap , contentInset ]
314311 )
315312
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+ }
319319 }
320- } , [ index . value , onTabPress , tabNamesArray ] )
320+ )
321321
322322 const pageScrollHandler = usePageScrollHandler ( {
323323 onPageScroll : ( e ) => {
@@ -381,7 +381,7 @@ export const Container = React.memo(
381381 >
382382 < Animated . View
383383 style = { [ styles . container , { width } , containerStyle ] }
384- onLayout = { onLayout }
384+ onLayout = { getContainerLayoutHeight }
385385 pointerEvents = "box-none"
386386 >
387387 < Animated . View
@@ -430,7 +430,7 @@ export const Container = React.memo(
430430 < AnimatedPagerView
431431 ref = { containerRef }
432432 onPageScroll = { pageScrollHandler }
433- initialPage = { index . value }
433+ initialPage = { initialIndex }
434434 { ...pagerProps }
435435 style = { [ pagerProps ?. style , StyleSheet . absoluteFill ] }
436436 >
0 commit comments