Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Libraries/Animated/NativeFabricMeasurerTurboModule.js
Original file line number Diff line number Diff line change
@@ -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<Spec>(
'NativeFabricMeasurerTurboModule',
): ?Spec);
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -380,6 +383,7 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() {
ImageStoreManager.class,
IntentModule.class,
NativeAnimatedModule.class,
NativeFabricMeasurerModule.class,
NetworkingModule.class,
PermissionsModule.class,
DevToolsSettingsManagerModule.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}

35 changes: 34 additions & 1 deletion ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
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.

// 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 =
Expand Down Expand Up @@ -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});
Expand Down