Skip to content

Commit

Permalink
NativeMeasurer for Android
Browse files Browse the repository at this point in the history
  • Loading branch information
Flewp committed Oct 5, 2023
1 parent 905f731 commit 29649f6
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 1 deletion.
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()) {
// 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

0 comments on commit 29649f6

Please sign in to comment.