Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
In This PR..
A fix for measuring native views in Fabric.
Background
The Shadow Tree
Starting in Fabric, React Native has a "Shadow Tree". Its state is kept in C++. The Shadow Tree is the Native representation of React's JS component tree. The Shadow Tree differs from React's JS component tree in two main ways.
<MyCoolComponent style={...}><View /></MyCoolComponent>
in JS,the Shadow Tree would only contain
View
becauseMyCoolComponent
does not have a Native representation (unless you added a nativeViewManager
forMyCoolComponent
and registered that natively ahead of time, but we're assuming here the only reference toMyCoolComponent
is in JS).React Native's
View.Animated
React Native provides an
Animated
API. Its purpose is to be able to provide smooth animations defined in JS for components ultimately rendered on the Native side. Because historically React Native's JS and Native communication happened via the "JSON bridge" asynchronously, there wasn't a performant way to pipe an event reliably every frame to smoothly animate views, so they developed a way to directly manipulate a view's layout parameters, bypassng React's rendering commit/layout pipeline.In the Paper renderer (a.k.a. the "Old Architecture"), React was able to get away with its Animated API bypassing the render pipeline because Paper's equivalent of the "Shadow Tree" was literally just the native
View
tree. As in, when React wanted to get a NativeView
's measurement from the JS side, the Native side's implementation would start at theView
in question, walk up through each parent until it found aView
that had the typeRootView
, then walk back down the childrenView
s until it got to the originalView
, applying all the parentView
offsets along the way. All that is to say, theView
s could be updated via React's rendering pipeline, or directly manipulated through theAnimated
API, and it didn't matter because the NativeView
tree was the source of truth.The Problem
In the Fabric renderer (a.k.a. the "New Architecture"), the JS side now considers the Native source of truth to be the "Shadow Tree". As in, when the JS side wants to measure a
View
, it only goes to check the Shadow Tree's layout information. As far as the Native side is concerned, it no longer directly services measurement requests. Unfortunately, React Native'sAnimated
API still bypasses the React renderer, so any layout updates applied by theAnimated
API never get committed to the Shadow Tree. This becomes problematic when a view needs to be measured from the JS side. Probably the easiest example to illustrate for this is React Native's pressability architecture. When a press gesture is being processed, the logic measures the native view so it can check if touch events stay within the native view's bounds. If the native view has been manipulated by React Native'sAnimated
API in any way, those manipulated properties aren't reflected in the Shadow Tree and so the views on the screen aren't necessarily where the Shadow Tree thinks they are. So when the JS side checks the Shadow Tree and sees that the touch event is not within the bounds of the view (even though it looks like it is on the phone's screen), it cancels the press event.A fairly straightforward upshot: any place in JS that uses
UIManager.measure
orUIManager.measureInWindow
is at risk of being reported incorrect measurements from the Shadow Tree when on Fabric.Here are some related discussion threads where others are finding similar issues.
facebook#36504
facebook#36710
Here's a couple places that we've seen
Animated
APIs:Navigator
becausereact-native-screens
animates each screen in aNavigator
directly withAnimated
.The Fix
Both the
Animated
API andUIManager.measure*
are used within React Native itself and several third party dependencies. Therefore, my initial approach here is to create a TurboModule that implements a retrofitted version of Paper's measure logic using a bit of Fabric's UI Manager on the native side, and polyfill the measure methods on the JS side to hopefully manipulate allmeasure*
calls to funnel into the proper measure logic.Related Reading
measure
.getRelativeLayoutMetrics
calls into the Shadow Tree.measure
and theNativeViewHierarchyManager
. Notice how it's just walking up Android'sView
tree. This is what this PR's TurboModule is inspired by.SurfaceMountingManager
, which is how Fabric manipulates the Native View tree. It's basically the equivalent of Paper'sNativeHierarchyManager
. With Paper, theNativeHierarchyManager
has much more information passed to it. Now in Fabric, theSurfaceMountingManager
is passed essentially minimal diffs offered by the Shadow Tree.Animated
provides an escape hatch prop. See howpassthroughAnimatedPropExplicitValues
is formed. This approach probably wouldn't work for us because it basically runs that prop through the JS's render loop, causing lag even with Fabric.SurfaceMountingManager
, or backward up to JS to theuseAnimatedStyle
hook. Reanimated is able to performantly run animations directly through manipulating the Shadow Tree. This allows animations serviced by Reanimated to not be affected by this problem.Testing Plan
I've tested that this fixes pressable surfaces on Fabric devices. Also tested that this fix doesn't regress things on Fabric.