Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ export default class Pressability {
if (typeof this._responderID === 'number') {
UIManager.measure(this._responderID, this._measureCallback);
} else {
this._responderID.measure(this._measureCallback);
this._responderID.measureAsyncOnUI(this._measureCallback);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export default class ReactFabricHostComponent implements NativeMethods {
}
}

measureAsyncOnUI(callback: MeasureOnSuccessCallback): void {
// Note: this is only implemented in ReactNativeElement with NativeDOM + enableAccessToHostTreeInFabric
this.measure(callback);
}

unstable_getBoundingClientRect(): DOMRect {
const node = getNodeFromInternalInstanceHandle(
this.__internalInstanceHandle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#import <UIKit/UIKit.h>

#import <React/RCTFabricSurface.h>
#import <React/RCTMountingManagerDelegate.h>
#import <React/RCTPrimitives.h>
#import <react/renderer/core/ComponentDescriptor.h>
Expand Down Expand Up @@ -69,6 +70,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)synchronouslyUpdateViewOnUIThread:(ReactTag)reactTag
changedProps:(folly::dynamic)props
componentDescriptor:(const facebook::react::ComponentDescriptor &)componentDescriptor;

- (void)measureAsyncOnUI:(ReactTag)reactTag
rootView:(UIView *)rootView
callback:(const std::function<void(folly::dynamic)> &)callback;
@end

NS_ASSUME_NONNULL_END
26 changes: 26 additions & 0 deletions packages/react-native/React/Fabric/Mounting/RCTMountingManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,30 @@ - (void)synchronouslyDispatchAccessbilityEventOnUIThread:(ReactTag)reactTag even
}
}

- (void)measureAsyncOnUI:(ReactTag)reactTag rootView:(UIView*)rootView callback:(const std::function<void (folly::dynamic)> &)callback {
RCTAssertMainQueue();

UIView<RCTComponentViewProtocol> *view = [self->_componentViewRegistry findComponentViewWithTag:reactTag];
if (!view) {
// this view was probably collapsed out
RCTLogWarn(@"measure cannot find view with tag #%@", reactTag);
callback({});
return;
}

// By convention, all coordinates, whether they be touch coordinates, or
// measurement coordinates are with respect to the root view.
CGRect frame = view.frame;
CGRect globalBounds = [view convertRect:view.bounds toView:rootView];

callback(
folly::dynamic::array(frame.origin.x,
frame.origin.y,
globalBounds.size.width,
globalBounds.size.height,
globalBounds.origin.x,
globalBounds.origin.y)
);
}

@end
3 changes: 3 additions & 0 deletions packages/react-native/React/Fabric/RCTScheduler.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ NS_ASSUME_NONNULL_BEGIN
forShadowView:(const facebook::react::ShadowView &)shadowView;

- (void)schedulerDidSynchronouslyUpdateViewOnUIThread:(facebook::react::Tag)reactTag props:(folly::dynamic)props;

- (void)schedulerMeasureAsyncOnUI:(const facebook::react::ShadowView &)shadowView
callback:(const std::function<void(folly::dynamic)> &)callback;
@end

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native/React/Fabric/RCTScheduler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ void schedulerDidUpdateShadowTree(const std::unordered_map<Tag, folly::dynamic>
// Does nothing.
// This delegate method is not currently used on iOS.
}

void schedulerMeasureAsyncOnUI(const ShadowView& shadowView, const std::function<void(folly::dynamic)> &callback) override {
RCTScheduler *scheduler = (__bridge RCTScheduler *)scheduler_;
[scheduler.delegate schedulerMeasureAsyncOnUI:shadowView callback:callback];
}

private:
void *scheduler_;
Expand Down
13 changes: 13 additions & 0 deletions packages/react-native/React/Fabric/RCTSurfacePresenter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,19 @@ - (void)schedulerDidSetIsJSResponder:(BOOL)isJSResponder
[_mountingManager setIsJSResponder:isJSResponder blockNativeResponder:blockNativeResponder forShadowView:shadowView];
}

- (void)schedulerMeasureAsyncOnUI:(const facebook::react::ShadowView &)shadowView callback:(const std::function<void (folly::dynamic)> &)callback {
ReactTag tag = shadowView.tag;
SurfaceId surfaceId = shadowView.surfaceId;
RCTFabricSurface *surface = [self surfaceForRootTag:surfaceId];

std::function<void (folly::dynamic)> callbackCopy = callback;
RCTExecuteOnMainQueue(^{
UIView *rootView = surface.view;
[self->_mountingManager measureAsyncOnUI:tag rootView:rootView callback:callbackCopy];
});
}


- (void)addObserver:(id<RCTSurfacePresenterObserver>)observer
{
std::unique_lock lock(_observerListMutex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.core.util.Preconditions;
Expand All @@ -34,6 +35,7 @@
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.infer.annotation.ThreadConfined;
import com.facebook.proguard.annotations.DoNotStripAny;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ColorPropConverter;
import com.facebook.react.bridge.GuardedRunnable;
import com.facebook.react.bridge.LifecycleEventListener;
Expand Down Expand Up @@ -1291,6 +1293,44 @@ public String toString() {
});
}

public void measureAsyncOnUI(int surfaceId, int reactTag, final Callback callback) {
mMountItemDispatcher.addMountItem(
new MountItem() {
@Override
public void execute(@NonNull MountingManager mountingManager) {
int[] mMeasureBuffer = new int[4];
boolean result = mountingManager.measureAsyncOnUI(
surfaceId,
reactTag,
mMeasureBuffer
);

if (!result) {
// TODO: add error handling callback
return;
}

double x = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
double y = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
double width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
double height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);

callback.invoke(0, 0, width, height, x, y);
}

@Override
public int getSurfaceId() {
return surfaceId;
}

@NonNull
@Override
public String toString() {
return "MEASURE_VIEW";
}
});
}

@Override
public void profileNextBatch() {
// TODO T31905686: Remove this method and add support for multi-threading performance counters
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.fabric.mounting

import android.graphics.RectF
import android.view.View
import android.view.ViewParent

public object MeasureAsyncUtil {
private val mBoundingBox = RectF()

/**
* Output buffer will be {x, y, width, height}.
*/
public fun measure(rootView: View, viewToMeasure: View, outputBuffer: IntArray) {
computeBoundingBox(rootView, outputBuffer)
val rootX = outputBuffer[0]
val rootY = outputBuffer[1]
computeBoundingBox(viewToMeasure, outputBuffer)
outputBuffer[0] -= rootX
outputBuffer[1] -= rootY
}

private fun computeBoundingBox(view: View, outputBuffer: IntArray) {
mBoundingBox.set(0f, 0f, view.width.toFloat(), view.height.toFloat())
mapRectFromViewToWindowCoords(view, mBoundingBox)

outputBuffer[0] = Math.round(mBoundingBox.left)
outputBuffer[1] = Math.round(mBoundingBox.top)
outputBuffer[2] = Math.round(mBoundingBox.right - mBoundingBox.left)
outputBuffer[3] = Math.round(mBoundingBox.bottom - mBoundingBox.top)
}

private fun mapRectFromViewToWindowCoords(view: View, rect: RectF) {
var matrix = view.getMatrix()
if (!matrix.isIdentity) {
matrix.mapRect(rect)
}

rect.offset(view.left.toFloat(), view.top.toFloat())

var parent: ViewParent? = view.parent
while (parent is View) {
val parentView = parent as View

rect.offset(-parentView.scrollX.toFloat(), -parentView.scrollY.toFloat())

matrix = parentView.getMatrix()
if (!matrix.isIdentity) {
matrix.mapRect(rect)
}

rect.offset(parentView.left.toFloat(), parentView.top.toFloat())

parent = parentView.parent
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.facebook.react.bridge.WritableMap
import com.facebook.react.fabric.events.EventEmitterWrapper
import com.facebook.react.fabric.mounting.mountitems.MountItem
import com.facebook.react.touch.JSResponderHandler
import com.facebook.react.uimanager.IllegalViewOperationException
import com.facebook.react.uimanager.RootViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerRegistry
Expand Down Expand Up @@ -333,6 +334,32 @@ internal class MountingManager(
attachmentsPositions,
)

@UiThread
@ThreadConfined(ThreadConfined.UI)
fun measureAsyncOnUI(
surfaceId: Int,
reactTag: Int,
outputBuffer: IntArray,
): Boolean {
val smm = getSurfaceManagerEnforced(surfaceId, "measure")
val view = try {
smm.getView(reactTag);
} catch (ex: IllegalViewOperationException) {
FLog.e(TAG, "Failed to find view for tag: %d. Error: %s", reactTag, ex.message);
return false
}

val rootView = try {
smm.getView(surfaceId)
} catch (ex: IllegalViewOperationException) {
FLog.e(TAG, "Failed to find root view for surfaceId: %d. Error: %s", surfaceId, ex.message);
return false
}

MeasureAsyncUtil.measure(rootView, view, outputBuffer)
return true;
}

fun enqueuePendingEvent(
surfaceId: Int,
reactTag: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <cxxreact/TraceSection.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/jni/ReadableNativeMap.h>
#include <react/jni/JCallback.h>
#include <react/renderer/components/scrollview/ScrollViewProps.h>
#include <react/renderer/core/DynamicPropsUtilities.h>
#include <react/renderer/core/conversions.h>
Expand Down Expand Up @@ -1082,4 +1083,16 @@ void FabricMountingManager::synchronouslyUpdateViewOnUIThread(
synchronouslyUpdateViewOnUIThreadJNI(javaUIManager_, viewTag, propsMap);
}

void FabricMountingManager::measureAsyncOnUI(
const ShadowView& shadowView,
const std::function<void(folly::dynamic)>& callback) {
static auto measureJNI =
JFabricUIManager::javaClassStatic()->getMethod<void(jint, jint, jni::alias_ref<JCallback>)>(
"measureAsyncOnUI");

auto javaCallback = JCxxCallbackImpl::newObjectCxxArgs(callback);

measureJNI(javaUIManager_, shadowView.surfaceId, shadowView.tag, javaCallback);
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class FabricMountingManager final {
Tag viewTag,
const folly::dynamic& props);

void measureAsyncOnUI(
const ShadowView& shadowView,
const std::function<void(folly::dynamic)>& callback);

private:
bool isOnMainThread();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,17 @@ void FabricUIManagerBinding::schedulerDidUpdateShadowTree(
// no-op
}

void FabricUIManagerBinding::schedulerMeasureAsyncOnUI(
const facebook::react::ShadowView& shadowView,
const std::function<void(folly::dynamic)>& callback) {
auto mountingManager = getMountingManager("schedulerMeasureAsyncOnUI");
if (!mountingManager) {
return;
}

mountingManager->measureAsyncOnUI(shadowView, callback);
}

void FabricUIManagerBinding::onAnimationStarted() {
auto mountingManager = getMountingManager("onAnimationStarted");
if (!mountingManager) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ class FabricUIManagerBinding : public jni::HybridClass<FabricUIManagerBinding>,
void schedulerDidUpdateShadowTree(
const std::unordered_map<Tag, folly::dynamic>& tagToProps) override;

void schedulerMeasureAsyncOnUI(
const ShadowView& shadowView,
const std::function<void(folly::dynamic)>& callback) override;

void setPixelDensity(float pointScaleFactor);

void driveCxxAnimations();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,30 @@ void NativeDOM::measureLayout(
onSuccess(rect.x, rect.y, rect.width, rect.height);
}

void NativeDOM::measureAsyncOnUI(
jsi::Runtime& rt,
std::shared_ptr<const ShadowNode> shadowNode,
const MeasureAsyncOnUICallback& callback) {
UIManager& uiManager = getUIManagerFromRuntime(rt);
UIManagerDelegate* uiManagerDelegate = uiManager.getDelegate();
if (uiManagerDelegate == nullptr) {
return;
}

std::function<void(folly::dynamic)> jsCallback = [callback](const folly::dynamic& args) {
// TODO: can we make the rest accept an AsyncFunction directly?
callback.call(
args.at(0).getDouble(),
args.at(1).getDouble(),
args.at(2).getDouble(),
args.at(3).getDouble(),
args.at(4).getDouble(),
args.at(5).getDouble());
};

uiManagerDelegate->uiManagerMeasureAsyncOnUI(shadowNode, jsCallback);
}

#pragma mark - Legacy direct manipulation APIs (for `ReactNativeElement`).

void NativeDOM::setNativeProps(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ using MeasureInWindowOnSuccessCallback =
using MeasureLayoutOnSuccessCallback =
SyncCallback<void(double, double, double, double)>;

using MeasureAsyncOnUICallback =
AsyncCallback<double, double, double, double, double, double>;

class NativeDOM : public NativeDOMCxxSpec<NativeDOM> {
public:
NativeDOM(std::shared_ptr<CallInvoker> jsInvoker);
Expand Down Expand Up @@ -143,6 +146,11 @@ class NativeDOM : public NativeDOMCxxSpec<NativeDOM> {
jsi::Function onFail,
const MeasureLayoutOnSuccessCallback& onSuccess);

void measureAsyncOnUI(
jsi::Runtime& rt,
std::shared_ptr<const ShadowNode> shadowNode,
const MeasureAsyncOnUICallback& callback);

#pragma mark - Legacy direct manipulation APIs (for `ReactNativeElement`).

void setNativeProps(
Expand Down
Loading
Loading