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

Fallback to Shadow Tree measurement if native measure fails #49

Open
wants to merge 11 commits into
base: 0.72.3-discord-1
Choose a base branch
from
5 changes: 3 additions & 2 deletions Libraries/Animated/NativeFabricMeasurerTurboModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ type MeasureInWindowOnSuccessCallback = (
) => void;

export interface Spec extends TurboModule {
+measureNatively: (viewTag: number, callback: MeasureOnSuccessCallback) => void,
+measureNatively: (viewTag: number, successCallback: MeasureOnSuccessCallback, failCallback: (successCallback: MeasureOnSuccessCallback) => void) => void,
+measureInWindowNatively: (
viewTag: number,
callback: MeasureInWindowOnSuccessCallback,
successCallback: MeasureInWindowOnSuccessCallback,
failCallback: (successCallback: MeasureInWindowOnSuccessCallback) => void
) => void,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.IllegalViewOperationException;
import com.facebook.react.uimanager.NativeViewMeasurer;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.UIManagerHelper;
Expand All @@ -24,26 +25,37 @@ public NativeFabricMeasurerModule(ReactApplicationContext reactContext) {
}

@Override
public void measureNatively(double viewTag, Callback callback) {
public void measureNatively(double viewTag, Callback onSuccess, Callback onFail) {
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);
try {
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]);
onSuccess.invoke(0, 0, width, height, x, y);
}
catch(IllegalViewOperationException e) {
// To avoid a scoping bug in UIManagerBinding.cpp, we need to pass the successCallback back here.
onFail.invoke(onSuccess);
}
});
}

@Override
public void measureInWindowNatively(double viewTag, Callback callback) {
public void measureInWindowNatively(double viewTag, Callback onSuccess, Callback onFail) {
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);
try {
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]);
onSuccess.invoke(x, y, width, height);
}
catch (IllegalViewOperationException e) {
onFail.invoke(onSuccess);
Copy link

Choose a reason for hiding this comment

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

If keeping this approach, I'd put the above comment about scoping here too.

}
});
}

Expand Down
219 changes: 152 additions & 67 deletions ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,63 @@ void UIManagerBinding::invalidate() const {
uiManager_->setDelegate(nullptr);
}

void measureFromShadowTree(
Copy link
Author

Choose a reason for hiding this comment

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

I wanted to break this work out into a method. It's my understanding c++ doesn't allow you to introduce functions inside functions, so I had to do it here. Is that right? Is there not a way for this code to show up closer to where it's used?

Copy link

Choose a reason for hiding this comment

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

This seems correct. You could maybe move it closer by making a lambda, or a macro, but those introduce their own set of tradeoffs.

You probably want to make this a proper private method of the UIManagerBinding class though.

UIManager *uiManager,
ShadowNode::Shared shadowNode,
jsi::Function &onSuccessFunction,
jsi::Runtime &runtime) {
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNode, nullptr, {/* .includeTransform = */ true});

if (layoutMetrics == EmptyLayoutMetrics) {
onSuccessFunction.call(runtime, {0, 0, 0, 0, 0, 0});
return;
}
auto newestCloneOfShadowNode =
uiManager->getNewestCloneOfShadowNode(*shadowNode);

auto layoutableShadowNode =
traitCast<LayoutableShadowNode const *>(newestCloneOfShadowNode.get());
Point originRelativeToParent = layoutableShadowNode != nullptr
? layoutableShadowNode->getLayoutMetrics().frame.origin
: Point();

auto frame = layoutMetrics.frame;
onSuccessFunction.call(
runtime,
{jsi::Value{runtime, (double)originRelativeToParent.x},
jsi::Value{runtime, (double)originRelativeToParent.y},
jsi::Value{runtime, (double)frame.size.width},
jsi::Value{runtime, (double)frame.size.height},
jsi::Value{runtime, (double)frame.origin.x},
jsi::Value{runtime, (double)frame.origin.y}});
}

void measureInWindowFromShadowTree(
UIManager *uiManager,
ShadowNode::Shared shadowNode,
jsi::Function &onSuccessFunction,
jsi::Runtime &runtime) {
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNode,
nullptr,
{/* .includeTransform = */ true,
/* .includeViewportOffset = */ true});

if (layoutMetrics == EmptyLayoutMetrics) {
onSuccessFunction.call(runtime, {0, 0, 0, 0});
return;
}

auto frame = layoutMetrics.frame;
onSuccessFunction.call(
runtime,
{jsi::Value{runtime, (double)frame.origin.x},
jsi::Value{runtime, (double)frame.origin.y},
jsi::Value{runtime, (double)frame.size.width},
jsi::Value{runtime, (double)frame.size.height}});
}

jsi::Value UIManagerBinding::get(
jsi::Runtime &runtime,
jsi::PropNameID const &name) {
Expand Down Expand Up @@ -576,49 +633,62 @@ 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 nativeMeasurerValue =
runtime.global()
.getProperty(runtime, "__turboModuleProxy")
.asObject(runtime)
.asFunction(runtime)
.call(runtime, "NativeFabricMeasurerTurboModule");
auto onSuccessFunction =
Copy link

Choose a reason for hiding this comment

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

Move this into the if statement

arguments[1].getObject(runtime).getFunction(runtime);

if (layoutMetrics == EmptyLayoutMetrics) {
onSuccessFunction.call(runtime, {0, 0, 0, 0, 0, 0});
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(),
// measureNatively takes two callbacks, one for if the
// layout is found and one for if an error occurs so we
// can gracefully fallback to measuring out of the
// shadow tree.
onSuccessFunction,
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(
runtime, "onNativeMeasureFail"),
1,
[uiManager, shadowNode](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
// There's a bug that occurs if, below, we try to
// call the onSuccessFunction we extracted
// earlier. It seems that JSIFunctions can't be
// passed into capture clauses for c++ anonymous
// functions, because they lack the appropriate
// copy constructor. Since we do definitely need
// access to the onSuccessFunction, we obtain that
// access by passing it into the .call() above and
// then passing it BACK OUT in native land, so we
// can extract it from args and use it right here.
auto onSuccessFunction =
args[0].getObject(rt).getFunction(rt);
measureFromShadowTree(
uiManager, shadowNode, onSuccessFunction, rt);
return jsi::Value::undefined();
}));
return jsi::Value::undefined();
}
auto newestCloneOfShadowNode =
uiManager->getNewestCloneOfShadowNode(*shadowNode);

auto layoutableShadowNode = traitCast<LayoutableShadowNode const *>(
newestCloneOfShadowNode.get());
Point originRelativeToParent = layoutableShadowNode != nullptr
? layoutableShadowNode->getLayoutMetrics().frame.origin
: Point();
measureFromShadowTree(
uiManager, shadowNode, onSuccessFunction, runtime);

auto frame = layoutMetrics.frame;
onSuccessFunction.call(
runtime,
{jsi::Value{runtime, (double)originRelativeToParent.x},
jsi::Value{runtime, (double)originRelativeToParent.y},
jsi::Value{runtime, (double)frame.size.width},
jsi::Value{runtime, (double)frame.size.height},
jsi::Value{runtime, (double)frame.origin.x},
jsi::Value{runtime, (double)frame.origin.y}});
return jsi::Value::undefined();
});
}
Expand All @@ -634,43 +704,58 @@ 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, "measureInWindowNatively")
.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,
/* .includeViewportOffset = */ true});

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

Choose a reason for hiding this comment

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

Same as the if comment above

arguments[1].getObject(runtime).getFunction(runtime);

if (layoutMetrics == EmptyLayoutMetrics) {
onSuccessFunction.call(runtime, {0, 0, 0, 0});
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(),
onSuccessFunction,
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(
runtime, "onNativeMeasureFail"),
Copy link

Choose a reason for hiding this comment

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

createFromHostFunction is decorating the JS runtime, and I'm not sure what the behavior would be if you had conflicting names. I'd rename this to something like onNativeMeasureInWindowFail

1,
[uiManager, shadowNode](
jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) {
// There's a bug that occurs if, below, we try to
// call the onSuccessFunction we extracted
// earlier. It seems that JSIFunctions can't be
// passed into capture clauses for c++ anonymous
// functions, because they lack the appropriate
// copy constructor. Since we do definitely need
// access to the onSuccessFunction, we obtain that
// access by passing it into the .call() above and
// then passing it BACK OUT in native land, so we
// can extract it from args and use it right here.
auto onSuccessFunction =
args[0].getObject(rt).getFunction(rt);
measureInWindowFromShadowTree(
uiManager, shadowNode, onSuccessFunction, rt);
return jsi::Value::undefined();
}));
return jsi::Value::undefined();
}

auto frame = layoutMetrics.frame;
onSuccessFunction.call(
runtime,
{jsi::Value{runtime, (double)frame.origin.x},
jsi::Value{runtime, (double)frame.origin.y},
jsi::Value{runtime, (double)frame.size.width},
jsi::Value{runtime, (double)frame.size.height}});
measureInWindowFromShadowTree(
uiManager, shadowNode, onSuccessFunction, runtime);

return jsi::Value::undefined();
});
}
Expand Down