Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NativeMeasurer for Android #44

Merged
merged 1 commit into from
Oct 10, 2023
Merged

NativeMeasurer for Android #44

merged 1 commit into from
Oct 10, 2023

Conversation

Flewp
Copy link

@Flewp Flewp commented Oct 5, 2023

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.

  1. The Shadow tree only contains the components that reduce to actual Native views. For example, if you had a
    <MyCoolComponent style={...}><View /></MyCoolComponent> in JS,
    the Shadow Tree would only contain View because MyCoolComponent does not have a Native representation (unless you added a native ViewManager for MyCoolComponent and registered that natively ahead of time, but we're assuming here the only reference to MyCoolComponent is in JS).
  2. The Shadow Tree contains every view's layout information (provided by Yoga). This includes the x/y coordinates and width/height. Importantly to this issue, this layout information is provided/updated every time a new version of the Shadow Tree is "committed" (usually "commits" are kicked off after React's reconciliation process). This doc is incredibly helpful related reading for the commit process..

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 Native View's measurement from the JS side, the Native side's implementation would start at the View in question, walk up through each parent until it found a View that had the type RootView, then walk back down the children Views until it got to the original View, applying all the parent View offsets along the way. All that is to say, the Views could be updated via React's rendering pipeline, or directly manipulated through the Animated API, and it didn't matter because the Native View 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's Animated API still bypasses the React renderer, so any layout updates applied by the Animated 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's Animated 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 or UIManager.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:

  • In the Navigator because react-native-screens animates each screen in a Navigator directly with Animated.
  • In Alerts
  • In ScrollViews

The Fix

Both the Animated API and UIManager.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 all measure* calls to funnel into the proper measure logic.

Related Reading

Testing Plan

I've tested that this fixes pressable surfaces on Fabric devices. Also tested that this fix doesn't regress things on Fabric.

auto nativeMeasurerValue = runtime.global().getProperty(runtime, "__turboModuleProxy")
.asObject(runtime).asFunction(runtime).call(runtime, "NativeFabricMeasurerTurboModule");

if (nativeMeasurerValue.isObject()) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check will return false for Paper?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! Luckily this entire file is only used for Fabric, so we don't need to check for Paper anywhere here.

@Flewp Flewp merged commit c3c6e17 into 0.72.3-discord-1 Oct 10, 2023
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants