diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 1afe5763e44891..50667b644454d9 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -3746,10 +3746,8 @@ public class com/facebook/react/runtime/ReactSurfaceImpl : com/facebook/react/in public fun updateInitProps (Landroid/os/Bundle;)V } -public class com/facebook/react/runtime/ReactSurfaceView : com/facebook/react/ReactRootView { +public final class com/facebook/react/runtime/ReactSurfaceView : com/facebook/react/ReactRootView { public fun (Landroid/content/Context;Lcom/facebook/react/runtime/ReactSurfaceImpl;)V - protected fun dispatchJSPointerEvent (Landroid/view/MotionEvent;Z)V - protected fun dispatchJSTouchEvent (Landroid/view/MotionEvent;)V public fun getCurrentReactContext ()Lcom/facebook/react/bridge/ReactContext; public fun getUIManagerType ()I public fun handleException (Ljava/lang/Throwable;)V @@ -3758,8 +3756,6 @@ public class com/facebook/react/runtime/ReactSurfaceView : com/facebook/react/Re public fun isViewAttachedToReactInstance ()Z public fun onChildEndedNativeGesture (Landroid/view/View;Landroid/view/MotionEvent;)V public fun onChildStartedNativeGesture (Landroid/view/View;Landroid/view/MotionEvent;)V - protected fun onLayout (ZIIII)V - protected fun onMeasure (II)V public fun requestDisallowInterceptTouchEvent (Z)V public fun setIsFabric (Z)V } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.java deleted file mode 100644 index f261cafec7c761..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.java +++ /dev/null @@ -1,248 +0,0 @@ -/* - * 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.runtime; - -import android.content.Context; -import android.graphics.Point; -import android.graphics.Rect; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewParent; -import androidx.annotation.Nullable; -import com.facebook.common.logging.FLog; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.ReactRootView; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.config.ReactFeatureFlags; -import com.facebook.react.uimanager.IllegalViewOperationException; -import com.facebook.react.uimanager.JSPointerDispatcher; -import com.facebook.react.uimanager.JSTouchDispatcher; -import com.facebook.react.uimanager.common.UIManagerType; -import com.facebook.react.uimanager.events.EventDispatcher; -import com.facebook.systrace.Systrace; -import java.util.Objects; - -/** A view created by {@link ReactSurface} that's responsible for rendering a React component. */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public class ReactSurfaceView extends ReactRootView { - - private static final String TAG = "ReactSurfaceView"; - - private final ReactSurfaceImpl mSurface; - - private final JSTouchDispatcher mJSTouchDispatcher; - private @Nullable JSPointerDispatcher mJSPointerDispatcher; - - private boolean mWasMeasured = false; - private int mWidthMeasureSpec = 0; - private int mHeightMeasureSpec = 0; - - public ReactSurfaceView(Context context, ReactSurfaceImpl surface) { - super(context); - mSurface = surface; - mJSTouchDispatcher = new JSTouchDispatcher(this); - if (ReactFeatureFlags.dispatchPointerEvents) { - mJSPointerDispatcher = new JSPointerDispatcher(this); - } - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactSurfaceView.onMeasure"); - - int width = 0; - int height = 0; - int widthMode = MeasureSpec.getMode(widthMeasureSpec); - if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - int childSize = - child.getLeft() - + child.getMeasuredWidth() - + child.getPaddingLeft() - + child.getPaddingRight(); - width = Math.max(width, childSize); - } - } else { - width = MeasureSpec.getSize(widthMeasureSpec); - } - int heightMode = MeasureSpec.getMode(heightMeasureSpec); - if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - int childSize = - child.getTop() - + child.getMeasuredHeight() - + child.getPaddingTop() - + child.getPaddingBottom(); - height = Math.max(height, childSize); - } - } else { - height = MeasureSpec.getSize(heightMeasureSpec); - } - setMeasuredDimension(width, height); - - mWasMeasured = true; - mWidthMeasureSpec = widthMeasureSpec; - mHeightMeasureSpec = heightMeasureSpec; - - Point viewportOffset = getViewportOffset(); - - mSurface.updateLayoutSpecs( - mWidthMeasureSpec, mHeightMeasureSpec, viewportOffset.x, viewportOffset.y); - - Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - // Call updateLayoutSpecs to update locationOnScreen offsets, in case they've changed - if (mWasMeasured && changed) { - Point viewportOffset = getViewportOffset(); - mSurface.updateLayoutSpecs( - mWidthMeasureSpec, mHeightMeasureSpec, viewportOffset.x, viewportOffset.y); - } - } - - private Point getViewportOffset() { - int[] locationOnScreen = new int[2]; - getLocationOnScreen(locationOnScreen); - - // we need to subtract visibleWindowCoords - to subtract possible window insets, split - // screen or multi window - Rect visibleWindowFrame = new Rect(); - getWindowVisibleDisplayFrame(visibleWindowFrame); - locationOnScreen[0] -= visibleWindowFrame.left; - locationOnScreen[1] -= visibleWindowFrame.top; - - return new Point(locationOnScreen[0], locationOnScreen[1]); - } - - @Override - public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { - // Override in order to still receive events to onInterceptTouchEvent even when some other - // views disallow that, but propagate it up the tree if possible. - ViewParent parent = getParent(); - if (parent != null) { - parent.requestDisallowInterceptTouchEvent(disallowIntercept); - } - } - - /** - * Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called - * from the child's onTouchIntercepted implementation. - */ - @Override - public void onChildStartedNativeGesture(View childView, MotionEvent ev) { - EventDispatcher eventDispatcher = mSurface.getEventDispatcher(); - if (eventDispatcher == null) { - return; - } - - if (mJSTouchDispatcher != null) { - mJSTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher); - } - if (childView != null && mJSPointerDispatcher != null) { - mJSPointerDispatcher.onChildStartedNativeGesture(childView, ev, eventDispatcher); - } - } - - @Override - public void onChildEndedNativeGesture(View childView, MotionEvent ev) { - EventDispatcher eventDispatcher = mSurface.getEventDispatcher(); - if (eventDispatcher == null) { - return; - } - - if (mJSTouchDispatcher != null && mSurface.getEventDispatcher() != null) { - mJSTouchDispatcher.onChildEndedNativeGesture(ev, mSurface.getEventDispatcher()); - } - if (mJSPointerDispatcher != null) { - mJSPointerDispatcher.onChildStartedNativeGesture(childView, ev, eventDispatcher); - } - } - - @Override - public void handleException(Throwable t) { - ReactHostImpl reactHost = mSurface.getReactHost(); - if (reactHost != null) { - String errorMessage = Objects.toString(t.getMessage(), ""); - Exception e = new IllegalViewOperationException(errorMessage, this, t); - reactHost.handleHostException(e); - } - } - - @Override - public void setIsFabric(boolean isFabric) { - // This surface view is always on Fabric regardless. - super.setIsFabric(true); - } - - @Override - public @UIManagerType int getUIManagerType() { - // This surface view is always on Fabric. - return UIManagerType.FABRIC; - } - - @Override - protected void dispatchJSTouchEvent(MotionEvent event) { - if (mJSTouchDispatcher == null) { - FLog.w(TAG, "Unable to dispatch touch events to JS before the dispatcher is available"); - return; - } - EventDispatcher eventDispatcher = mSurface.getEventDispatcher(); - if (eventDispatcher != null) { - mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher); - } else { - FLog.w( - TAG, "Unable to dispatch touch events to JS as the React instance has not been attached"); - } - } - - @Override - protected void dispatchJSPointerEvent(MotionEvent event, boolean isCapture) { - if (mJSPointerDispatcher == null) { - if (!ReactFeatureFlags.dispatchPointerEvents) { - return; - } - FLog.w(TAG, "Unable to dispatch pointer events to JS before the dispatcher is available"); - return; - } - EventDispatcher eventDispatcher = mSurface.getEventDispatcher(); - if (eventDispatcher != null) { - mJSPointerDispatcher.handleMotionEvent(event, eventDispatcher, isCapture); - } else { - FLog.w( - TAG, - "Unable to dispatch pointer events to JS as the React instance has not been attached"); - } - } - - @Override - public boolean hasActiveReactContext() { - return mSurface.isAttached() && mSurface.getReactHost().getCurrentReactContext() != null; - } - - @Override - public boolean hasActiveReactInstance() { - return mSurface.isAttached() && mSurface.getReactHost().isInstanceInitialized(); - } - - @Override - public @Nullable ReactContext getCurrentReactContext() { - if (mSurface.isAttached()) { - return mSurface.getReactHost().getCurrentReactContext(); - } - return null; - } - - @Override - public boolean isViewAttachedToReactInstance() { - return mSurface.isAttached(); - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt new file mode 100644 index 00000000000000..ad2cb69d4629d7 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/runtime/ReactSurfaceView.kt @@ -0,0 +1,184 @@ +/* + * 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. + */ + +@file:Suppress("DEPRECATION") + +package com.facebook.react.runtime + +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.view.MotionEvent +import android.view.View +import com.facebook.common.logging.FLog +import com.facebook.react.ReactRootView +import com.facebook.react.bridge.ReactContext +import com.facebook.react.config.ReactFeatureFlags +import com.facebook.react.uimanager.IllegalViewOperationException +import com.facebook.react.uimanager.JSPointerDispatcher +import com.facebook.react.uimanager.JSTouchDispatcher +import com.facebook.react.uimanager.common.UIManagerType +import com.facebook.systrace.Systrace +import java.util.Objects + +/** A view created by [ReactSurface] that's responsible for rendering a React component. */ +public class ReactSurfaceView(context: Context?, private val surface: ReactSurfaceImpl) : + ReactRootView(context) { + private val jsTouchDispatcher: JSTouchDispatcher = JSTouchDispatcher(this) + private var jsPointerDispatcher: JSPointerDispatcher? = null + private var wasMeasured = false + private var widthMeasureSpec = 0 + private var heightMeasureSpec = 0 + + init { + if (ReactFeatureFlags.dispatchPointerEvents) { + jsPointerDispatcher = JSPointerDispatcher(this) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ReactSurfaceView.onMeasure") + var width = 0 + var height = 0 + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + if (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) { + for (i in 0 until childCount) { + val child = getChildAt(i) + val childSize = (child.left + child.measuredWidth + child.paddingLeft + child.paddingRight) + width = Math.max(width, childSize) + } + } else { + width = MeasureSpec.getSize(widthMeasureSpec) + } + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + for (i in 0 until childCount) { + val child = getChildAt(i) + val childSize = (child.top + child.measuredHeight + child.paddingTop + child.paddingBottom) + height = Math.max(height, childSize) + } + } else { + height = MeasureSpec.getSize(heightMeasureSpec) + } + setMeasuredDimension(width, height) + wasMeasured = true + this.widthMeasureSpec = widthMeasureSpec + this.heightMeasureSpec = heightMeasureSpec + val viewportOffset = viewportOffset + surface.updateLayoutSpecs( + widthMeasureSpec, heightMeasureSpec, viewportOffset.x, viewportOffset.y) + Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + // Call updateLayoutSpecs to update locationOnScreen offsets, in case they've changed + if (wasMeasured && changed) { + val viewportOffset = viewportOffset + surface.updateLayoutSpecs( + widthMeasureSpec, heightMeasureSpec, viewportOffset.x, viewportOffset.y) + } + } + + private val viewportOffset: Point + get() { + val locationOnScreen = IntArray(2) + getLocationOnScreen(locationOnScreen) + + // we need to subtract visibleWindowCoords - to subtract possible window insets, split + // screen or multi window + val visibleWindowFrame = Rect() + getWindowVisibleDisplayFrame(visibleWindowFrame) + locationOnScreen[0] -= visibleWindowFrame.left + locationOnScreen[1] -= visibleWindowFrame.top + return Point(locationOnScreen[0], locationOnScreen[1]) + } + + override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { + // Override in order to still receive events to onInterceptTouchEvent even when some other + // views disallow that, but propagate it up the tree if possible. + parent?.requestDisallowInterceptTouchEvent(disallowIntercept) + } + /** + * Called when a child starts a native gesture (e.g. a scroll in a ScrollView). Should be called + * from the child's onTouchIntercepted implementation. + */ + override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) { + val eventDispatcher = surface.eventDispatcher ?: return + jsTouchDispatcher.onChildStartedNativeGesture(ev, eventDispatcher) + if (childView != null && jsPointerDispatcher != null) { + jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher) + } + } + + override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) { + val eventDispatcher = surface.eventDispatcher ?: return + if (surface.eventDispatcher != null) { + jsTouchDispatcher.onChildEndedNativeGesture(ev, eventDispatcher) + } + jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, eventDispatcher) + } + + override fun handleException(t: Throwable) { + val reactHost = surface.reactHost + val errorMessage = Objects.toString(t.message, "") + val e: Exception = IllegalViewOperationException(errorMessage, this, t) + reactHost.handleHostException(e) + } + + override fun setIsFabric(isFabric: Boolean) { + // This surface view is always on Fabric regardless. + super.setIsFabric(true) + } + + // This surface view is always on Fabric. + @UIManagerType override fun getUIManagerType(): Int = UIManagerType.FABRIC + + override fun dispatchJSTouchEvent(event: MotionEvent) { + val eventDispatcher = surface.eventDispatcher + if (eventDispatcher != null) { + jsTouchDispatcher.handleTouchEvent(event, eventDispatcher) + } else { + FLog.w( + TAG, "Unable to dispatch touch events to JS as the React instance has not been attached") + } + } + + override fun dispatchJSPointerEvent(event: MotionEvent, isCapture: Boolean) { + if (jsPointerDispatcher == null) { + if (!ReactFeatureFlags.dispatchPointerEvents) { + return + } + FLog.w(TAG, "Unable to dispatch pointer events to JS before the dispatcher is available") + return + } + val eventDispatcher = surface.eventDispatcher + if (eventDispatcher != null) { + jsPointerDispatcher?.handleMotionEvent(event, eventDispatcher, isCapture) + } else { + FLog.w( + TAG, + "Unable to dispatch pointer events to JS as the React instance has not been attached") + } + } + + override fun hasActiveReactContext(): Boolean = + surface.isAttached && surface.reactHost.currentReactContext != null + + override fun hasActiveReactInstance(): Boolean = + surface.isAttached && surface.reactHost.isInstanceInitialized + + override fun getCurrentReactContext(): ReactContext? = + if (surface.isAttached) { + surface.reactHost.currentReactContext + } else null + + override fun isViewAttachedToReactInstance(): Boolean = surface.isAttached + + private companion object { + private const val TAG = "ReactSurfaceView" + } +}