NativeMeasurer for Android #44
Merged
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
ViewbecauseMyCoolComponentdoes not have a Native representation (unless you added a nativeViewManagerforMyCoolComponentand registered that natively ahead of time, but we're assuming here the only reference toMyCoolComponentis in JS).React Native's
View.AnimatedReact Native provides an
AnimatedAPI. 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
Viewtree. As in, when React wanted to get a NativeView's measurement from the JS side, the Native side's implementation would start at theViewin question, walk up through each parent until it found aViewthat had the typeRootView, then walk back down the childrenViews until it got to the originalView, applying all the parentViewoffsets along the way. All that is to say, theViews could be updated via React's rendering pipeline, or directly manipulated through theAnimatedAPI, and it didn't matter because the NativeViewtree 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'sAnimatedAPI still bypasses the React renderer, so any layout updates applied by theAnimatedAPI 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'sAnimatedAPI 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.measureorUIManager.measureInWindowis 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
AnimatedAPIs:Navigatorbecausereact-native-screensanimates each screen in aNavigatordirectly withAnimated.The Fix
Both the
AnimatedAPI 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.getRelativeLayoutMetricscalls into the Shadow Tree.measureand theNativeViewHierarchyManager. Notice how it's just walking up Android'sViewtree. 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, theNativeHierarchyManagerhas much more information passed to it. Now in Fabric, theSurfaceMountingManageris passed essentially minimal diffs offered by the Shadow Tree.Animatedprovides an escape hatch prop. See howpassthroughAnimatedPropExplicitValuesis 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 theuseAnimatedStylehook. 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.