Skip to content

Commit c3c6e17

Browse files
authored
Merge pull request #44 from discord/native-measurer
NativeMeasurer for Android
2 parents 905f731 + 29649f6 commit c3c6e17

File tree

5 files changed

+257
-1
lines changed

5 files changed

+257
-1
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type {TurboModule} from '../TurboModule/RCTExport';
2+
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
3+
4+
type MeasureOnSuccessCallback = (
5+
x: number,
6+
y: number,
7+
width: number,
8+
height: number,
9+
pageX: number,
10+
pageY: number,
11+
) => void;
12+
13+
type MeasureInWindowOnSuccessCallback = (
14+
x: number,
15+
y: number,
16+
width: number,
17+
height: number,
18+
) => void;
19+
20+
export interface Spec extends TurboModule {
21+
+measureNatively: (viewTag: number, callback: MeasureOnSuccessCallback) => void,
22+
+measureInWindowNatively: (
23+
viewTag: number,
24+
callback: MeasureInWindowOnSuccessCallback,
25+
) => void,
26+
}
27+
28+
export default (TurboModuleRegistry.get<Spec>(
29+
'NativeFabricMeasurerTurboModule',
30+
): ?Spec);
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.facebook.react.animated;
2+
3+
import android.util.Log;
4+
import android.view.View;
5+
6+
import com.facebook.fbreact.specs.NativeFabricMeasurerTurboModuleSpec;
7+
import com.facebook.react.bridge.Callback;
8+
import com.facebook.react.bridge.ReactApplicationContext;
9+
import com.facebook.react.bridge.UIManager;
10+
import com.facebook.react.bridge.UiThreadUtil;
11+
import com.facebook.react.bridge.WritableNativeMap;
12+
import com.facebook.react.module.annotations.ReactModule;
13+
import com.facebook.react.uimanager.NativeViewMeasurer;
14+
import com.facebook.react.uimanager.PixelUtil;
15+
import com.facebook.react.uimanager.UIManagerHelper;
16+
import com.facebook.react.uimanager.common.UIManagerType;
17+
18+
@ReactModule(name = NativeFabricMeasurerTurboModuleSpec.NAME)
19+
public class NativeFabricMeasurerModule extends NativeFabricMeasurerTurboModuleSpec implements NativeViewMeasurer.ViewProvider {
20+
private final NativeViewMeasurer measurer = new NativeViewMeasurer(this);
21+
22+
public NativeFabricMeasurerModule(ReactApplicationContext reactContext) {
23+
super(reactContext);
24+
}
25+
26+
@Override
27+
public void measureNatively(double viewTag, Callback callback) {
28+
getReactApplicationContext().runOnUiQueueThread(() -> {
29+
int[] output = measurer.measure((int) viewTag);
30+
float x = PixelUtil.toDIPFromPixel(output[0]);
31+
float y = PixelUtil.toDIPFromPixel(output[1]);
32+
float width = PixelUtil.toDIPFromPixel(output[2]);
33+
float height = PixelUtil.toDIPFromPixel(output[3]);
34+
callback.invoke(0, 0, width, height, x, y);
35+
});
36+
}
37+
38+
@Override
39+
public void measureInWindowNatively(double viewTag, Callback callback) {
40+
getReactApplicationContext().runOnUiQueueThread(() -> {
41+
int[] output = measurer.measureInWindow((int) viewTag);
42+
float x = PixelUtil.toDIPFromPixel(output[0]);
43+
float y = PixelUtil.toDIPFromPixel(output[1]);
44+
float width = PixelUtil.toDIPFromPixel(output[2]);
45+
float height = PixelUtil.toDIPFromPixel(output[3]);
46+
callback.invoke(x, y, width, height);
47+
});
48+
}
49+
50+
@Override
51+
public View provideView(int tag) {
52+
UIManager uiManager = UIManagerHelper.getUIManager(getReactApplicationContext(), UIManagerType.FABRIC);
53+
if (uiManager == null) {
54+
return null;
55+
}
56+
57+
return uiManager.resolveView(tag);
58+
}
59+
}

ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.facebook.react.TurboReactPackage;
1212
import com.facebook.react.ViewManagerOnDemandReactPackage;
1313
import com.facebook.react.animated.NativeAnimatedModule;
14+
import com.facebook.react.animated.NativeFabricMeasurerModule;
1415
import com.facebook.react.bridge.ModuleSpec;
1516
import com.facebook.react.bridge.NativeModule;
1617
import com.facebook.react.bridge.ReactApplicationContext;
@@ -131,6 +132,8 @@ public MainReactPackage(MainPackageConfig config) {
131132
return new IntentModule(context);
132133
case NativeAnimatedModule.NAME:
133134
return new NativeAnimatedModule(context);
135+
case NativeFabricMeasurerModule.NAME:
136+
return new NativeFabricMeasurerModule(context);
134137
case NetworkingModule.NAME:
135138
return new NetworkingModule(context);
136139
case PermissionsModule.NAME:
@@ -380,6 +383,7 @@ public ReactModuleInfoProvider getReactModuleInfoProvider() {
380383
ImageStoreManager.class,
381384
IntentModule.class,
382385
NativeAnimatedModule.class,
386+
NativeFabricMeasurerModule.class,
383387
NetworkingModule.class,
384388
PermissionsModule.class,
385389
DevToolsSettingsManagerModule.class,
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
package com.facebook.react.uimanager;
8+
9+
import android.graphics.Matrix;
10+
import android.graphics.Rect;
11+
import android.graphics.RectF;
12+
import android.view.View;
13+
import android.view.ViewParent;
14+
15+
import com.facebook.common.logging.FLog;
16+
import com.facebook.react.bridge.UiThreadUtil;
17+
18+
public class NativeViewMeasurer {
19+
public static final String TAG = "NativeViewMeasurer";
20+
private final ViewProvider viewProvider;
21+
public NativeViewMeasurer(ViewProvider viewProvider) {
22+
this.viewProvider = viewProvider;
23+
}
24+
25+
/**
26+
* Returns true on success, false on failure. If successful, after calling, output buffer will be
27+
* {x, y, width, height}.
28+
*/
29+
public int[] measure(int tag) {
30+
UiThreadUtil.assertOnUiThread();
31+
32+
int[] outputBuffer = {0, 0, 0, 0, 0, 0};
33+
View v = viewProvider.provideView(tag);
34+
if (v == null) {
35+
FLog.w(TAG, "measure: No native view for " + tag + " currently exists");
36+
return outputBuffer;
37+
}
38+
39+
View rootView = (View) RootViewUtil.getRootView(v);
40+
// It is possible that the RootView can't be found because this view is no longer on the screen
41+
// and has been removed by clipping
42+
if (rootView == null) {
43+
FLog.w(TAG, "measure: Native view " + tag + " is no longer on screen");
44+
return outputBuffer;
45+
}
46+
47+
computeBoundingBox(rootView, outputBuffer);
48+
int rootX = outputBuffer[0];
49+
int rootY = outputBuffer[1];
50+
computeBoundingBox(v, outputBuffer);
51+
outputBuffer[0] -= rootX;
52+
outputBuffer[1] -= rootY;
53+
return outputBuffer;
54+
}
55+
56+
/**
57+
* Returns the coordinates of a view relative to the window (not just the RootView which is what
58+
* measure will return)
59+
*
60+
* @param tag - the tag for the view
61+
*/
62+
public int[] measureInWindow(int tag) {
63+
UiThreadUtil.assertOnUiThread();
64+
View v = viewProvider.provideView(tag);
65+
int[] outputBuffer = {0, 0, 0, 0};
66+
if (v == null) {
67+
FLog.w(TAG, "measureInWindow: No native view for " + tag + " currently exists");
68+
return outputBuffer;
69+
}
70+
71+
int[] locationOutputBuffer = new int[2];
72+
v.getLocationOnScreen(locationOutputBuffer);
73+
74+
// we need to subtract visibleWindowCoords - to subtract possible window insets, split screen or
75+
// multi window
76+
Rect visibleWindowFrame = new Rect();
77+
v.getWindowVisibleDisplayFrame(visibleWindowFrame);
78+
outputBuffer[0] = locationOutputBuffer[0] - visibleWindowFrame.left;
79+
outputBuffer[1] = locationOutputBuffer[1] - visibleWindowFrame.top;
80+
81+
// outputBuffer[0,1] already contain what we want
82+
outputBuffer[2] = v.getWidth();
83+
outputBuffer[3] = v.getHeight();
84+
return outputBuffer;
85+
}
86+
87+
private void computeBoundingBox(View view, int[] outputBuffer) {
88+
RectF boundingBox = new RectF(0, 0, view.getWidth(), view.getHeight());
89+
boundingBox.set(0, 0, view.getWidth(), view.getHeight());
90+
mapRectFromViewToWindowCoords(view, boundingBox);
91+
92+
outputBuffer[0] = Math.round(boundingBox.left);
93+
outputBuffer[1] = Math.round(boundingBox.top);
94+
outputBuffer[2] = Math.round(boundingBox.right - boundingBox.left);
95+
outputBuffer[3] = Math.round(boundingBox.bottom - boundingBox.top);
96+
outputBuffer[4] = Math.round(view.getLeft());
97+
outputBuffer[5] = Math.round(view.getTop());
98+
}
99+
100+
private void mapRectFromViewToWindowCoords(View view, RectF rect) {
101+
Matrix matrix = view.getMatrix();
102+
if (!matrix.isIdentity()) {
103+
matrix.mapRect(rect);
104+
}
105+
106+
rect.offset(view.getLeft(), view.getTop());
107+
108+
ViewParent parent = view.getParent();
109+
while (parent instanceof View) {
110+
View parentView = (View) parent;
111+
112+
rect.offset(-parentView.getScrollX(), -parentView.getScrollY());
113+
114+
matrix = parentView.getMatrix();
115+
if (!matrix.isIdentity()) {
116+
matrix.mapRect(rect);
117+
}
118+
119+
rect.offset(parentView.getLeft(), parentView.getTop());
120+
121+
parent = parentView.getParent();
122+
}
123+
}
124+
125+
126+
public interface ViewProvider {
127+
View provideView(int tag);
128+
}
129+
}
130+

ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,22 @@ jsi::Value UIManagerBinding::get(
576576
jsi::Value const *arguments,
577577
size_t /*count*/) noexcept -> jsi::Value {
578578
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
579+
bool turboModuleCalled = false;
580+
auto nativeMeasurerValue = runtime.global().getProperty(runtime, "__turboModuleProxy")
581+
.asObject(runtime).asFunction(runtime).call(runtime, "NativeFabricMeasurerTurboModule");
582+
583+
if (nativeMeasurerValue.isObject()) {
584+
// This calls measureNatively if the NativeFabricMeasurerTurboModule is found.
585+
// The return value doesn't matter here because the measure values will be passed through the callback.
586+
jsi::Value returnValue = nativeMeasurerValue.asObject(runtime).getPropertyAsFunction(runtime, "measureNatively")
587+
.call(runtime, shadowNode.get()->getTag(), arguments[1].getObject(runtime).getFunction(runtime));
588+
turboModuleCalled = true;
589+
}
590+
591+
if (turboModuleCalled) {
592+
return jsi::Value::undefined();
593+
}
594+
579595
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
580596
*shadowNode, nullptr, {/* .includeTransform = */ true});
581597
auto onSuccessFunction =
@@ -617,8 +633,25 @@ jsi::Value UIManagerBinding::get(
617633
jsi::Value const & /*thisValue*/,
618634
jsi::Value const *arguments,
619635
size_t /*count*/) noexcept -> jsi::Value {
636+
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
637+
bool turboModuleCalled = false;
638+
auto nativeMeasurerValue = runtime.global().getProperty(runtime, "__turboModuleProxy")
639+
.asObject(runtime).asFunction(runtime).call(runtime, "NativeFabricMeasurerTurboModule");
640+
641+
if (nativeMeasurerValue.isObject()) {
642+
// This calls measureNatively if the NativeFabricMeasurerTurboModule is found.
643+
// The return value doesn't matter here because the measure values will be passed through the callback.
644+
jsi::Value returnValue = nativeMeasurerValue.asObject(runtime).getPropertyAsFunction(runtime, "measureInWindowNatively")
645+
.call(runtime, shadowNode.get()->getTag(), arguments[1].getObject(runtime).getFunction(runtime));
646+
turboModuleCalled = true;
647+
}
648+
649+
if (turboModuleCalled) {
650+
return jsi::Value::undefined();
651+
}
652+
620653
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
621-
*shadowNodeFromValue(runtime, arguments[0]),
654+
*shadowNode,
622655
nullptr,
623656
{/* .includeTransform = */ true,
624657
/* .includeViewportOffset = */ true});

0 commit comments

Comments
 (0)