diff --git a/Libraries/Animated/NativeFabricMeasurerTurboModule.js b/Libraries/Animated/NativeFabricMeasurerTurboModule.js new file mode 100644 index 00000000000000..1cca2aed05fd1d --- /dev/null +++ b/Libraries/Animated/NativeFabricMeasurerTurboModule.js @@ -0,0 +1,30 @@ +import type {TurboModule} from '../TurboModule/RCTExport'; +import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry'; + +type MeasureOnSuccessCallback = ( + x: number, + y: number, + width: number, + height: number, + pageX: number, + pageY: number, +) => void; + +type MeasureInWindowOnSuccessCallback = ( + x: number, + y: number, + width: number, + height: number, +) => void; + +export interface Spec extends TurboModule { + +measureNatively: (viewTag: number, callback: MeasureOnSuccessCallback) => void, + +measureInWindowNatively: ( + viewTag: number, + callback: MeasureInWindowOnSuccessCallback, + ) => void, +} + +export default (TurboModuleRegistry.get( + 'NativeFabricMeasurerTurboModule', +): ?Spec); diff --git a/ReactAndroid/src/main/java/com/facebook/react/animated/NativeFabricMeasurerModule.java b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeFabricMeasurerModule.java new file mode 100644 index 00000000000000..87ae22c52f0c00 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/animated/NativeFabricMeasurerModule.java @@ -0,0 +1,59 @@ +package com.facebook.react.animated; + +import android.util.Log; +import android.view.View; + +import com.facebook.fbreact.specs.NativeFabricMeasurerTurboModuleSpec; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.UIManager; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.NativeViewMeasurer; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.common.UIManagerType; + +@ReactModule(name = NativeFabricMeasurerTurboModuleSpec.NAME) +public class NativeFabricMeasurerModule extends NativeFabricMeasurerTurboModuleSpec implements NativeViewMeasurer.ViewProvider { + private final NativeViewMeasurer measurer = new NativeViewMeasurer(this); + + public NativeFabricMeasurerModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public void measureNatively(double viewTag, Callback callback) { + getReactApplicationContext().runOnUiQueueThread(() -> { + int[] output = measurer.measure((int) viewTag); + float x = PixelUtil.toDIPFromPixel(output[0]); + float y = PixelUtil.toDIPFromPixel(output[1]); + float width = PixelUtil.toDIPFromPixel(output[2]); + float height = PixelUtil.toDIPFromPixel(output[3]); + callback.invoke(0, 0, width, height, x, y); + }); + } + + @Override + public void measureInWindowNatively(double viewTag, Callback callback) { + getReactApplicationContext().runOnUiQueueThread(() -> { + int[] output = measurer.measureInWindow((int) viewTag); + float x = PixelUtil.toDIPFromPixel(output[0]); + float y = PixelUtil.toDIPFromPixel(output[1]); + float width = PixelUtil.toDIPFromPixel(output[2]); + float height = PixelUtil.toDIPFromPixel(output[3]); + callback.invoke(x, y, width, height); + }); + } + + @Override + public View provideView(int tag) { + UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), UIManagerType.FABRIC); + if (uiManager == null) { + return null; + } + + return uiManager.resolveView(tag); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java index 5fa72a0ffd02b0..b35275c4abc685 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java +++ b/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java @@ -11,6 +11,7 @@ import com.facebook.react.TurboReactPackage; import com.facebook.react.ViewManagerOnDemandReactPackage; import com.facebook.react.animated.NativeAnimatedModule; +import com.facebook.react.animated.NativeFabricMeasurerModule; import com.facebook.react.bridge.ModuleSpec; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; @@ -131,6 +132,8 @@ public MainReactPackage(MainPackageConfig config) { return new IntentModule(context); case NativeAnimatedModule.NAME: return new NativeAnimatedModule(context); + case NativeFabricMeasurerModule.NAME: + return new NativeFabricMeasurerModule(context); case NetworkingModule.NAME: return new NetworkingModule(context); case PermissionsModule.NAME: @@ -380,6 +383,7 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() { ImageStoreManager.class, IntentModule.class, NativeAnimatedModule.class, + NativeFabricMeasurerModule.class, NetworkingModule.class, PermissionsModule.class, DevToolsSettingsManagerModule.class, diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewMeasurer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewMeasurer.java new file mode 100644 index 00000000000000..e7565c358ec3b4 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewMeasurer.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +package com.facebook.react.uimanager; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.View; +import android.view.ViewParent; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.UiThreadUtil; + +public class NativeViewMeasurer { + public static final String TAG = "NativeViewMeasurer"; + private final ViewProvider viewProvider; + public NativeViewMeasurer(ViewProvider viewProvider) { + this.viewProvider = viewProvider; + } + + /** + * Returns true on success, false on failure. If successful, after calling, output buffer will be + * {x, y, width, height}. + */ + public int[] measure(int tag) { + UiThreadUtil.assertOnUiThread(); + + int[] outputBuffer = {0, 0, 0, 0, 0, 0}; + View v = viewProvider.provideView(tag); + if (v == null) { + FLog.w(TAG, "measure: No native view for " + tag + " currently exists"); + return outputBuffer; + } + + View rootView = (View) RootViewUtil.getRootView(v); + // It is possible that the RootView can't be found because this view is no longer on the screen + // and has been removed by clipping + if (rootView == null) { + FLog.w(TAG, "measure: Native view " + tag + " is no longer on screen"); + return outputBuffer; + } + + computeBoundingBox(rootView, outputBuffer); + int rootX = outputBuffer[0]; + int rootY = outputBuffer[1]; + computeBoundingBox(v, outputBuffer); + outputBuffer[0] -= rootX; + outputBuffer[1] -= rootY; + return outputBuffer; + } + + /** + * Returns the coordinates of a view relative to the window (not just the RootView which is what + * measure will return) + * + * @param tag - the tag for the view + */ + public int[] measureInWindow(int tag) { + UiThreadUtil.assertOnUiThread(); + View v = viewProvider.provideView(tag); + int[] outputBuffer = {0, 0, 0, 0}; + if (v == null) { + FLog.w(TAG, "measureInWindow: No native view for " + tag + " currently exists"); + return outputBuffer; + } + + int[] locationOutputBuffer = new int[2]; + v.getLocationOnScreen(locationOutputBuffer); + + // we need to subtract visibleWindowCoords - to subtract possible window insets, split screen or + // multi window + Rect visibleWindowFrame = new Rect(); + v.getWindowVisibleDisplayFrame(visibleWindowFrame); + outputBuffer[0] = locationOutputBuffer[0] - visibleWindowFrame.left; + outputBuffer[1] = locationOutputBuffer[1] - visibleWindowFrame.top; + + // outputBuffer[0,1] already contain what we want + outputBuffer[2] = v.getWidth(); + outputBuffer[3] = v.getHeight(); + return outputBuffer; + } + + private void computeBoundingBox(View view, int[] outputBuffer) { + RectF boundingBox = new RectF(0, 0, view.getWidth(), view.getHeight()); + boundingBox.set(0, 0, view.getWidth(), view.getHeight()); + mapRectFromViewToWindowCoords(view, boundingBox); + + outputBuffer[0] = Math.round(boundingBox.left); + outputBuffer[1] = Math.round(boundingBox.top); + outputBuffer[2] = Math.round(boundingBox.right - boundingBox.left); + outputBuffer[3] = Math.round(boundingBox.bottom - boundingBox.top); + outputBuffer[4] = Math.round(view.getLeft()); + outputBuffer[5] = Math.round(view.getTop()); + } + + private void mapRectFromViewToWindowCoords(View view, RectF rect) { + Matrix matrix = view.getMatrix(); + if (!matrix.isIdentity()) { + matrix.mapRect(rect); + } + + rect.offset(view.getLeft(), view.getTop()); + + ViewParent parent = view.getParent(); + while (parent instanceof View) { + View parentView = (View) parent; + + rect.offset(-parentView.getScrollX(), -parentView.getScrollY()); + + matrix = parentView.getMatrix(); + if (!matrix.isIdentity()) { + matrix.mapRect(rect); + } + + rect.offset(parentView.getLeft(), parentView.getTop()); + + parent = parentView.getParent(); + } + } + + + public interface ViewProvider { + View provideView(int tag); + } +} + diff --git a/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 27092d35f7c516..8608ab24ac5c69 100644 --- a/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -576,6 +576,22 @@ jsi::Value UIManagerBinding::get( jsi::Value const *arguments, size_t /*count*/) noexcept -> jsi::Value { auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + bool turboModuleCalled = false; + auto nativeMeasurerValue = runtime.global().getProperty(runtime, "__turboModuleProxy") + .asObject(runtime).asFunction(runtime).call(runtime, "NativeFabricMeasurerTurboModule"); + + if (nativeMeasurerValue.isObject()) { + // This calls measureNatively if the NativeFabricMeasurerTurboModule is found. + // The return value doesn't matter here because the measure values will be passed through the callback. + jsi::Value returnValue = nativeMeasurerValue.asObject(runtime).getPropertyAsFunction(runtime, "measureNatively") + .call(runtime, shadowNode.get()->getTag(), arguments[1].getObject(runtime).getFunction(runtime)); + turboModuleCalled = true; + } + + if (turboModuleCalled) { + return jsi::Value::undefined(); + } + auto layoutMetrics = uiManager->getRelativeLayoutMetrics( *shadowNode, nullptr, {/* .includeTransform = */ true}); auto onSuccessFunction = @@ -617,8 +633,25 @@ jsi::Value UIManagerBinding::get( jsi::Value const & /*thisValue*/, jsi::Value const *arguments, size_t /*count*/) noexcept -> jsi::Value { + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + bool turboModuleCalled = false; + auto nativeMeasurerValue = runtime.global().getProperty(runtime, "__turboModuleProxy") + .asObject(runtime).asFunction(runtime).call(runtime, "NativeFabricMeasurerTurboModule"); + + if (nativeMeasurerValue.isObject()) { + // This calls measureNatively if the NativeFabricMeasurerTurboModule is found. + // The return value doesn't matter here because the measure values will be passed through the callback. + jsi::Value returnValue = nativeMeasurerValue.asObject(runtime).getPropertyAsFunction(runtime, "measureInWindowNatively") + .call(runtime, shadowNode.get()->getTag(), arguments[1].getObject(runtime).getFunction(runtime)); + turboModuleCalled = true; + } + + if (turboModuleCalled) { + return jsi::Value::undefined(); + } + auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNodeFromValue(runtime, arguments[0]), + *shadowNode, nullptr, {/* .includeTransform = */ true, /* .includeViewportOffset = */ true});