diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.kt index 62f7ecfa9b3..b4cd837cf35 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEvent.kt @@ -10,10 +10,8 @@ package com.facebook.react.views.scroll import android.os.SystemClock import androidx.core.util.Pools.SynchronizedPool import com.facebook.infer.annotation.Assertions -import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactSoftExceptionLogger import com.facebook.react.bridge.WritableMap -import com.facebook.react.bridge.buildReadableMap import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel import com.facebook.react.uimanager.common.ViewUtil @@ -76,46 +74,24 @@ public class ScrollEvent private constructor() : Event() { override fun canCoalesce(): Boolean = scrollEventType == ScrollEventType.SCROLL override fun getEventData(): WritableMap { - val contentInset = buildReadableMap { - put("top", 0.0) - put("bottom", 0.0) - put("left", 0.0) - put("right", 0.0) - } - - val contentOffset = buildReadableMap { - put("x", toDIPFromPixel(scrollX).toDouble()) - put("y", toDIPFromPixel(scrollY).toDouble()) - } - - val contentSize = buildReadableMap { - put("width", toDIPFromPixel(contentWidth.toFloat()).toDouble()) - put("height", toDIPFromPixel(contentHeight.toFloat()).toDouble()) - } - - val layoutMeasurement = buildReadableMap { - put("width", toDIPFromPixel(scrollViewWidth.toFloat()).toDouble()) - put("height", toDIPFromPixel(scrollViewHeight.toFloat()).toDouble()) - } - - val velocity = buildReadableMap { - put("x", toDIPFromPixel(xVelocity).toDouble()) - put("y", toDIPFromPixel(yVelocity).toDouble()) - } - - val event = Arguments.createMap() - event.putMap("contentInset", contentInset) - event.putMap("contentOffset", contentOffset) - event.putMap("contentSize", contentSize) - event.putMap("layoutMeasurement", layoutMeasurement) - event.putMap("velocity", velocity) - event.putInt("target", viewTag) - event.putDouble("timestamp", timestamp.toDouble()) - event.putBoolean( - "responderIgnoreScroll", + val data = DoubleArray(ScrollEventHelper.FIELDS) + data[ScrollEventHelper.CONTENT_OFFSET_X_INDEX] = toDIPFromPixel(scrollX).toDouble() + data[ScrollEventHelper.CONTENT_OFFSET_Y_INDEX] = toDIPFromPixel(scrollY).toDouble() + data[ScrollEventHelper.CONTENT_WIDTH_INDEX] = toDIPFromPixel(contentWidth.toFloat()).toDouble() + data[ScrollEventHelper.CONTENT_HEIGHT_INDEX] = + toDIPFromPixel(contentHeight.toFloat()).toDouble() + data[ScrollEventHelper.LAYOUT_WIDTH_INDEX] = + toDIPFromPixel(scrollViewWidth.toFloat()).toDouble() + data[ScrollEventHelper.LAYOUT_HEIGHT_INDEX] = + toDIPFromPixel(scrollViewHeight.toFloat()).toDouble() + data[ScrollEventHelper.VELOCITY_X_INDEX] = toDIPFromPixel(xVelocity).toDouble() + data[ScrollEventHelper.VELOCITY_Y_INDEX] = toDIPFromPixel(yVelocity).toDouble() + data[ScrollEventHelper.TIMESTAMP_INDEX] = timestamp.toDouble() + return ScrollEventHelper.nativeCreateScrollPayload( + data, + viewTag, !ReactNativeFeatureFlags.shouldTriggerResponderTransferOnScrollAndroid(), ) - return event } public companion object { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEventHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEventHelper.kt new file mode 100644 index 00000000000..57e54f61cb3 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ScrollEventHelper.kt @@ -0,0 +1,47 @@ +/* + * 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.views.scroll + +import com.facebook.proguard.annotations.DoNotStrip +import com.facebook.react.bridge.ReactNativeJNISoLoader +import com.facebook.react.bridge.WritableNativeMap + +/** Builds the payload of a [ScrollEvent] in one native call. */ +@DoNotStrip +internal object ScrollEventHelper { + init { + ReactNativeJNISoLoader.staticInit() + } + + // Keep in sync with the field indices in ScrollEventHelper.cpp. + const val CONTENT_OFFSET_X_INDEX = 0 + const val CONTENT_OFFSET_Y_INDEX = 1 + const val CONTENT_WIDTH_INDEX = 2 + const val CONTENT_HEIGHT_INDEX = 3 + const val LAYOUT_WIDTH_INDEX = 4 + const val LAYOUT_HEIGHT_INDEX = 5 + const val VELOCITY_X_INDEX = 6 + const val VELOCITY_Y_INDEX = 7 + const val TIMESTAMP_INDEX = 8 + const val FIELDS = 9 + + /** + * Creates the payload of a scroll event: `contentInset` (all zero), `contentOffset`, + * `contentSize`, `layoutMeasurement`, `velocity`, `target`, `timestamp` and + * `responderIgnoreScroll`. + * + * @param data [FIELDS] values, in DIPs, at the indices above. + */ + @DoNotStrip + @JvmStatic + external fun nativeCreateScrollPayload( + data: DoubleArray, + target: Int, + responderIgnoreScroll: Boolean, + ): WritableNativeMap +} diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt index 1b58d0879f3..7ec40b218a0 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/CMakeLists.txt @@ -32,6 +32,7 @@ add_library( OnLoad-common.cpp ReadableNativeArray.cpp ReadableNativeMap.cpp + ScrollEventHelper.cpp TouchesHelper.cpp TransformHelper.cpp WritableNativeArray.cpp diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp index ba536d2cce3..85160a4fd34 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/OnLoad.cpp @@ -11,6 +11,7 @@ #include #include "InspectorNetworkRequestListener.h" +#include "ScrollEventHelper.h" #include "TouchesHelper.h" #include "TransformHelper.h" @@ -35,6 +36,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { FLAGS_minloglevel = 0; #endif InspectorNetworkRequestListener::registerNatives(); + ScrollEventHelper::registerNatives(); TouchesHelper::registerNatives(); TransformHelper::registerNatives(); }); diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/ScrollEventHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/jni/ScrollEventHelper.cpp new file mode 100644 index 00000000000..70d2cc3f0f4 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/ScrollEventHelper.cpp @@ -0,0 +1,81 @@ +/* + * 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. + */ + +#include "ScrollEventHelper.h" + +#include + +#include "WritableNativeMap.h" + +using namespace facebook::jni; + +namespace facebook::react { + +namespace { + +// Keep in sync with the field indices in ScrollEventHelper.kt. +constexpr jint kContentOffsetXIndex = 0; +constexpr jint kContentOffsetYIndex = 1; +constexpr jint kContentWidthIndex = 2; +constexpr jint kContentHeightIndex = 3; +constexpr jint kLayoutWidthIndex = 4; +constexpr jint kLayoutHeightIndex = 5; +constexpr jint kVelocityXIndex = 6; +constexpr jint kVelocityYIndex = 7; +constexpr jint kTimestampIndex = 8; +constexpr jint kFields = 9; + +local_ref createScrollPayload( + alias_ref /*unused*/, + alias_ref data, + jint target, + jboolean responderIgnoreScroll) { + if (data->size() != kFields) { + throw std::invalid_argument("Invalid scroll event data"); + } + auto values = data->getRegion(0, kFields); + const double* fields = values.get(); + + auto jPayload = WritableNativeMap::newObjectJavaArgs(); + auto* payload = jPayload->cthis(); + // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) + WritableNativeMap contentInset( + folly::dynamic::object("top", 0.0)("bottom", 0.0)("left", 0.0)( + "right", 0.0)); + payload->putNativeMap("contentInset", &contentInset); + WritableNativeMap contentOffset( + folly::dynamic::object("x", fields[kContentOffsetXIndex])( + "y", fields[kContentOffsetYIndex])); + payload->putNativeMap("contentOffset", &contentOffset); + WritableNativeMap contentSize( + folly::dynamic::object("width", fields[kContentWidthIndex])( + "height", fields[kContentHeightIndex])); + payload->putNativeMap("contentSize", &contentSize); + WritableNativeMap layoutMeasurement( + folly::dynamic::object("width", fields[kLayoutWidthIndex])( + "height", fields[kLayoutHeightIndex])); + payload->putNativeMap("layoutMeasurement", &layoutMeasurement); + WritableNativeMap velocity( + folly::dynamic::object("x", fields[kVelocityXIndex])( + "y", fields[kVelocityYIndex])); + payload->putNativeMap("velocity", &velocity); + payload->putInt("target", target); + payload->putDouble("timestamp", fields[kTimestampIndex]); + // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) + payload->putBoolean("responderIgnoreScroll", responderIgnoreScroll != 0u); + return jPayload; +} + +} // namespace + +void ScrollEventHelper::registerNatives() { + javaClassLocal()->registerNatives({ + makeNativeMethod("nativeCreateScrollPayload", createScrollPayload), + }); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/jni/ScrollEventHelper.h b/packages/react-native/ReactAndroid/src/main/jni/react/jni/ScrollEventHelper.h new file mode 100644 index 00000000000..7ae30445333 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/jni/ScrollEventHelper.h @@ -0,0 +1,21 @@ +/* + * 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. + */ + +#pragma once + +#include + +namespace facebook::react { + +class ScrollEventHelper : public jni::JavaClass { + public: + static auto constexpr *kJavaDescriptor = "Lcom/facebook/react/views/scroll/ScrollEventHelper;"; + + static void registerNatives(); +}; + +} // namespace facebook::react diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 80b0a4cabea..e03c23da774 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2203,6 +2203,11 @@ class facebook::react::SafeAreaViewProps : public facebook::react::HostPlatformV public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; } +class facebook::react::ScrollEventHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::Sealable { public Sealable(); public Sealable(const facebook::react::Sealable& other); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 8c77f19eabb..b9463dbd17f 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -3567,6 +3567,11 @@ class facebook::react::SchedulerDelegate { public virtual ~SchedulerDelegate() noexcept = default; } +class facebook::react::ScrollEventHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::ScrollViewEventEmitter : public facebook::react::BaseViewEventEmitter { public using EndDragMetrics = facebook::react::ScrollEndDragEvent; public using Metrics = facebook::react::ScrollEvent; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index d01f83280cf..cc356a256f3 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2202,6 +2202,11 @@ class facebook::react::SafeAreaViewProps : public facebook::react::HostPlatformV public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; } +class facebook::react::ScrollEventHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::Sealable { public bool getSealed() const; public void ensureUnsealed() const; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index d01f83280cf..cc356a256f3 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2202,6 +2202,11 @@ class facebook::react::SafeAreaViewProps : public facebook::react::HostPlatformV public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; } +class facebook::react::ScrollEventHelper : public facebook::jni::JavaClass { + public static constexpr auto * kJavaDescriptor; + public static void registerNatives(); +} + class facebook::react::Sealable { public bool getSealed() const; public void ensureUnsealed() const;