@@ -38,6 +38,22 @@ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("screen");
3838const SCROLL_SETTLE_MS = 400 ;
3939const VISIBLE_MARGIN = 16 ;
4040
41+ /**
42+ * Synchronous on Fabric via JSI. The callback fires inline,
43+ * so the return value is available immediately.
44+ */
45+ const measureInWindow = ( view : View ) : TourItemMeasurement | undefined => {
46+ const result : { value : TourItemMeasurement | undefined } = {
47+ value : undefined
48+ } ;
49+ view . measureInWindow ( ( x , y , width , height ) => {
50+ if ( width !== 0 || height !== 0 ) {
51+ result . value = { x, y, width, height } ;
52+ }
53+ } ) ;
54+ return result . value ;
55+ } ;
56+
4157export const TourOverlay = ( ) => {
4258 const { getMeasurement, getConfig, getScrollRef } = useTourContext ( ) ;
4359 const isActive = useIOSelector ( isTourActiveSelector ) ;
@@ -46,27 +62,25 @@ export const TourOverlay = () => {
4662 const items = useIOSelector ( tourItemsForActiveGroupSelector ) ;
4763
4864 const overlayRef = useRef < View > ( null ) ;
49- const overlayOffsetRef = useRef ( { x : 0 , y : 0 } ) ;
5065 const isFirstMeasurement = useRef ( true ) ;
5166 const measureGeneration = useRef ( 0 ) ;
5267
53- // Local visibility state: stays true during fade-out
5468 const [ visible , setVisible ] = useState ( false ) ;
5569
56- const cutoutX = useSharedValue ( 0 ) ;
57- const cutoutY = useSharedValue ( 0 ) ;
58- const cutoutW = useSharedValue ( 0 ) ;
59- const cutoutH = useSharedValue ( 0 ) ;
60- const opacity = useSharedValue ( 0 ) ;
61- const cutoutOpacity = useSharedValue ( 1 ) ;
62-
6370 const [ measurement , setMeasurement ] = useState <
6471 TourItemMeasurement | undefined
6572 > ( undefined ) ;
6673 const [ tooltipConfig , setTooltipConfig ] = useState <
6774 { title : string ; description : string } | undefined
6875 > ( undefined ) ;
6976
77+ const cutoutX = useSharedValue ( 0 ) ;
78+ const cutoutY = useSharedValue ( 0 ) ;
79+ const cutoutW = useSharedValue ( 0 ) ;
80+ const cutoutH = useSharedValue ( 0 ) ;
81+ const opacity = useSharedValue ( 0 ) ;
82+ const cutoutOpacity = useSharedValue ( 1 ) ;
83+
7084 // When tour becomes active, mount the overlay and reset first-measurement flag
7185 useEffect ( ( ) => {
7286 if ( isActive ) {
@@ -84,19 +98,6 @@ export const TourOverlay = () => {
8498 }
8599 } , [ isActive , visible , opacity ] ) ;
86100
87- const measureOverlayOffset = useCallback ( ( ) => {
88- if ( ! overlayRef . current ) {
89- return { x : 0 , y : 0 } ;
90- }
91- const result = { x : 0 , y : 0 } ;
92- overlayRef . current . measureInWindow ( ( x , y ) => {
93- result . x = x ;
94- result . y = y ;
95- } ) ;
96- overlayOffsetRef . current = result ;
97- return result ;
98- } , [ ] ) ;
99-
100101 const scrollIntoViewIfNeeded = useCallback (
101102 async (
102103 gId : string ,
@@ -170,25 +171,29 @@ export const TourOverlay = () => {
170171 config : { title : string ; description : string } | undefined ,
171172 didScroll : boolean
172173 ) => {
173- if ( isFirstMeasurement . current ) {
174- // First step: position cutout immediately, then fade the overlay in
175- isFirstMeasurement . current = false ;
174+ const updateStep = ( ) => {
176175 setMeasurement ( padded ) ;
177176 setTooltipConfig ( config ) ;
177+ } ;
178+
179+ const positionCutout = ( ) => {
178180 cutoutX . value = padded . x ;
179181 cutoutY . value = padded . y ;
180182 cutoutW . value = padded . width ;
181183 cutoutH . value = padded . height ;
184+ } ;
185+
186+ if ( isFirstMeasurement . current ) {
187+ // First step: position cutout immediately, then fade the overlay in
188+ isFirstMeasurement . current = false ;
189+ updateStep ( ) ;
190+ positionCutout ( ) ;
182191 cutoutOpacity . value = 1 ;
183192 opacity . value = withTiming ( 1 , { duration : ANIMATION_DURATION } ) ;
184193 } else if ( didScroll ) {
185194 // Already faded out before scrolling — reposition and fade in
186- cutoutX . value = padded . x ;
187- cutoutY . value = padded . y ;
188- cutoutW . value = padded . width ;
189- cutoutH . value = padded . height ;
190- setMeasurement ( padded ) ;
191- setTooltipConfig ( config ) ;
195+ positionCutout ( ) ;
196+ updateStep ( ) ;
192197 cutoutOpacity . value = withTiming ( 1 , {
193198 duration : ANIMATION_DURATION ,
194199 easing : STEP_EASING
@@ -198,10 +203,6 @@ export const TourOverlay = () => {
198203 // Cancel any lingering animation on cutoutOpacity from a previous
199204 // step to ensure the completion callback fires reliably.
200205 cancelAnimation ( cutoutOpacity ) ;
201- const updateStepUI = ( ) => {
202- setMeasurement ( padded ) ;
203- setTooltipConfig ( config ) ;
204- } ;
205206 cutoutOpacity . value = withTiming (
206207 0 ,
207208 { duration : ANIMATION_DURATION , easing : STEP_EASING } ,
@@ -210,7 +211,7 @@ export const TourOverlay = () => {
210211 cutoutY . value = padded . y ;
211212 cutoutW . value = padded . width ;
212213 cutoutH . value = padded . height ;
213- runOnJS ( updateStepUI ) ( ) ;
214+ runOnJS ( updateStep ) ( ) ;
214215 cutoutOpacity . value = withTiming ( 1 , {
215216 duration : ANIMATION_DURATION ,
216217 easing : STEP_EASING
@@ -254,11 +255,16 @@ export const TourOverlay = () => {
254255
255256 const { measurement : m , didScroll } = scrollResult ;
256257
257- const offset = measureOverlayOffset ( ) ;
258+ // Measure overlay position to convert page coords → overlay-relative coords
259+ const overlayOffset = overlayRef . current
260+ ? measureInWindow ( overlayRef . current )
261+ : undefined ;
262+ const ox = overlayOffset ?. x ?? 0 ;
263+ const oy = overlayOffset ?. y ?? 0 ;
258264
259265 const padded : TourItemMeasurement = {
260- x : m . x - offset . x - CUTOUT_PADDING ,
261- y : m . y - offset . y - CUTOUT_PADDING ,
266+ x : m . x - ox - CUTOUT_PADDING ,
267+ y : m . y - oy - CUTOUT_PADDING ,
262268 width : m . width + CUTOUT_PADDING * 2 ,
263269 height : m . height + CUTOUT_PADDING * 2
264270 } ;
@@ -272,7 +278,6 @@ export const TourOverlay = () => {
272278 getMeasurement ,
273279 getConfig ,
274280 scrollIntoViewIfNeeded ,
275- measureOverlayOffset ,
276281 applyCutout
277282 ] ) ;
278283
0 commit comments