Migrate Event class (#49887)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/49887

## Changelog:
[Internal] -

As in the title.

This was the last Java file in the `events-common` module, so now it can be a pure Kotlin one.

NOTE: I opted to make the base method `Event.getEventData` public (instead of previously protected), as based on the usage patterns by the users this seems to make the most sense in this context.

Reviewed By: cortinico

Differential Revision: D70777597

fbshipit-source-id: fdaa52e4400ad8e86a8711daf359eb5a10974d05
This commit is contained in:
Ruslan Shestopalyuk
2025-04-11 07:02:03 -07:00
committed by Facebook GitHub Bot
parent c134c41cd7
commit c50f3e5f66
12 changed files with 237 additions and 251 deletions
@@ -5036,6 +5036,7 @@ public abstract class com/facebook/react/uimanager/events/Event {
public fun coalesce (Lcom/facebook/react/uimanager/events/Event;)Lcom/facebook/react/uimanager/events/Event;
public fun dispatch (Lcom/facebook/react/uimanager/events/RCTEventEmitter;)V
public fun dispatchModern (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;)V
public final fun dispose ()V
protected fun experimental_isSynchronous ()Z
public fun getCoalescingKey ()S
public fun getEventAnimationDriverMatchSpec ()Lcom/facebook/react/uimanager/events/Event$EventAnimationDriverMatchSpec;
@@ -5044,11 +5045,12 @@ public abstract class com/facebook/react/uimanager/events/Event {
public abstract fun getEventName ()Ljava/lang/String;
public final fun getSurfaceId ()I
public final fun getTimestampMs ()J
public fun getUniqueID ()I
public final fun getUniqueID ()I
public final fun getViewTag ()I
protected fun init (I)V
protected fun init (II)V
protected fun init (IIJ)V
protected final fun init (I)V
protected final fun init (II)V
protected final fun init (IIJ)V
public final fun isInitialized ()Z
public fun onDispose ()V
}
@@ -515,7 +515,7 @@ public class NativeAnimatedNodesManager(
var foundAtLeastOneDriver = false
val matchSpec = event.eventAnimationDriverMatchSpec
for (driver in eventDrivers) {
if (matchSpec.match(driver.viewTag, driver.eventName)) {
if (matchSpec != null && matchSpec.match(driver.viewTag, driver.eventName)) {
foundAtLeastOneDriver = true
stopAnimationsForNode(driver.valueNode)
event.dispatchModern(driver)
@@ -323,7 +323,7 @@ public class ReactAccessibilityDelegate extends ExploreByTouchHelper {
}
@Override
protected WritableMap getEventData() {
public WritableMap getEventData() {
return event;
}
});
@@ -17,7 +17,7 @@ internal object BlackHoleEventDispatcher : EventDispatcher {
override fun dispatchEvent(event: Event<*>) {
FLog.d(
"BlackHoleEventDispatcher",
"Trying to emit event to JS, but the React instance isn't ready. Event: ${event.eventName}")
"Trying to emit event to JS, but the React instance isn't ready. Event: ${event.getEventName()}")
}
override fun dispatchAllEvents(): Unit = Unit
@@ -1,220 +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.uimanager.events;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Nullsafe;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.SystemClock;
/**
* A UI event that can be dispatched to JS.
*
* <p>For dispatching events {@code getEventData} should be used. Once event object is passed to the
* EventDispatched it should no longer be used as EventDispatcher may decide to recycle that object
* (by calling {@link #dispose}).
*
* <p>If you need advanced customizations and overriding only {@code getEventData} doesn't work for
* you, you must override both {@code dispatch} and {@code dispatchModern}. Both of these will be
* deleted in the distant future and it is highly recommended to use only {@code getEventData}.
*
* <p>Old, pre-Fabric Events only used viewTag as the identifier, but Fabric needs surfaceId as well
* as viewTag. You may use {@code UIManagerHelper.getSurfaceId} on a Fabric-managed View to get the
* surfaceId. Fabric will work without surfaceId - making {@code Event} backwards-compatible - but
* Events without SurfaceId are slightly slower to propagate.
*/
@Nullsafe(Nullsafe.Mode.LOCAL)
public abstract class Event<T extends Event> {
private static int sUniqueID = 0;
private boolean mInitialized;
private int mSurfaceId;
private int mViewTag;
private long mTimestampMs;
private int mUniqueID = sUniqueID++;
private @Nullable EventAnimationDriverMatchSpec mEventAnimationDriverMatchSpec;
protected Event() {}
@Deprecated
protected Event(int viewTag) {
init(viewTag);
}
protected Event(int surfaceId, int viewTag) {
init(surfaceId, viewTag);
}
@Deprecated
protected void init(int viewTag) {
init(-1, viewTag);
}
protected void init(int surfaceId, int viewTag) {
init(surfaceId, viewTag, SystemClock.uptimeMillis());
}
/**
* This method needs to be called before event is sent to event dispatcher. Event timestamps can
* optionally be dated/backdated to a custom time: for example, touch events should be dated with
* the system event time.
*/
protected void init(int surfaceId, int viewTag, long timestampMs) {
mSurfaceId = surfaceId;
mViewTag = viewTag;
mTimestampMs = timestampMs;
mInitialized = true;
}
/**
* @return the view id for the view that generated this event
*/
public final int getViewTag() {
return mViewTag;
}
/**
* @return the surfaceId for the view that generated this event
*/
public final int getSurfaceId() {
return mSurfaceId;
}
/**
* @return the time at which the event happened in the {@link android.os.SystemClock#uptimeMillis}
* base.
*/
public final long getTimestampMs() {
return mTimestampMs;
}
/**
* @return false if this Event can *never* be coalesced
*/
public boolean canCoalesce() {
return true;
}
/**
* Given two events, coalesce them into a single event that will be sent to JS instead of two
* separate events. By default, just chooses the one the is more recent, or {@code this} if
* timestamps are the same.
*
* <p>Two events will only ever try to be coalesced if they have the same event name, view id, and
* coalescing key.
*/
public T coalesce(T otherEvent) {
return (T) (getTimestampMs() >= otherEvent.getTimestampMs() ? this : otherEvent);
}
/**
* @return a key used to determine which other events of this type this event can be coalesced
* with. For example, touch move events should only be coalesced within a single gesture so a
* coalescing key there would be the unique gesture id.
*/
public short getCoalescingKey() {
return 0;
}
/**
* @return The unique id of this event.
*/
public int getUniqueID() {
return mUniqueID;
}
/**
* Called when the EventDispatcher is done with an event, either because it was dispatched or
* because it was coalesced with another Event.
*/
public void onDispose() {}
/*package*/ boolean isInitialized() {
return mInitialized;
}
/*package*/ final void dispose() {
mInitialized = false;
onDispose();
}
/**
* @return the name of this event as registered in JS
*/
public abstract String getEventName();
public EventAnimationDriverMatchSpec getEventAnimationDriverMatchSpec() {
if (mEventAnimationDriverMatchSpec == null) {
mEventAnimationDriverMatchSpec =
new EventAnimationDriverMatchSpec() {
@Override
public boolean match(int viewTag, String eventName) {
return viewTag == getViewTag() && eventName.equals(getEventName());
}
;
};
}
return mEventAnimationDriverMatchSpec;
}
/**
* Dispatch this event to JS using the given event emitter. Compatible with old and new renderer.
* Instead of using this or dispatchModern, it is recommended that you simply override
* `getEventData`.
*/
@Deprecated
public void dispatch(RCTEventEmitter rctEventEmitter) {
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), getEventData());
}
/** Can be overridden by classes when no custom logic for dispatching is needed. */
@Nullable
protected WritableMap getEventData() {
return null;
}
@EventCategoryDef
protected int getEventCategory() {
return EventCategoryDef.UNSPECIFIED;
}
protected boolean experimental_isSynchronous() {
return false;
}
/**
* Dispatch this event to JS using a V2 EventEmitter. If surfaceId is not -1 and `getEventData` is
* non-null, this will use the RCTModernEventEmitter API. Otherwise, it falls back to the
* old-style dispatch function. For Event classes that need to do something different, this method
* can always be overridden entirely, but it is not recommended.
*
* <p>This method additionally allows C++ to coalesce events and detect continuous ones for
* concurrent mode (Fabric only).
*
* @see #dispatch
*/
public void dispatchModern(RCTModernEventEmitter rctEventEmitter) {
if (getSurfaceId() != -1) {
rctEventEmitter.receiveEvent(
getSurfaceId(),
getViewTag(),
getEventName(),
canCoalesce(),
getCoalescingKey(),
getEventData(),
getEventCategory());
} else {
dispatch(rctEventEmitter);
}
}
public interface EventAnimationDriverMatchSpec {
boolean match(int viewTag, String eventName);
}
}
@@ -0,0 +1,201 @@
/*
* 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.uimanager.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.common.SystemClock.uptimeMillis
/**
* A UI event that can be dispatched to JS.
*
* For dispatching events `getEventData` should be used. Once event object is passed to the
* EventDispatcher it should no longer be used as EventDispatcher may decide to recycle that object
* (by calling [dispose]).
*
* If you need advanced customizations and overriding only `getEventData` doesn't work for you, you
* must override both `dispatch` and `dispatchModern`. Both of these will be deleted in the distant
* future and it is highly recommended to use only `getEventData`.
*
* Old, pre-Fabric Events only used viewTag as the identifier, but Fabric needs surfaceId as well as
* viewTag. You may use [UIManagerHelper.getSurfaceId] on a Fabric-managed View to get the
* surfaceId. Fabric will work without surfaceId - making [Event] backwards-compatible - but Events
* without SurfaceId are slightly slower to propagate.
*/
public abstract class Event<T : Event<T>> {
public var isInitialized: Boolean = false
private set
/** @return the surfaceId for the view that generated this event */
public var surfaceId: Int = 0
private set
/** @return the view id for the view that generated this event */
public var viewTag: Int = 0
private set
/**
* @return the time at which the event happened in the [android.os.SystemClock.uptimeMillis] base.
*/
public var timestampMs: Long = 0
private set
/** @return The unique id of this event. */
public val uniqueID: Int = uniqueIdCounter++
private var eventAnimationDriverMatchSpecCached: EventAnimationDriverMatchSpec? = null
protected constructor()
@Deprecated("Use constructor with explicit surfaceId instead")
protected constructor(viewTag: Int) {
init(viewTag)
}
protected constructor(surfaceId: Int, viewTag: Int) {
init(surfaceId, viewTag)
}
@Deprecated("Use version with explicit surfaceId instead")
protected fun init(viewTag: Int) {
init(-1, viewTag)
}
/**
* This method needs to be called before event is sent to event dispatcher. Event timestamps can
* optionally be dated/backdated to a custom time: for example, touch events should be dated with
* the system event time.
*/
protected fun init(surfaceId: Int, viewTag: Int, timestampMs: Long) {
this.surfaceId = surfaceId
this.viewTag = viewTag
this.timestampMs = timestampMs
this.isInitialized = true
}
protected fun init(surfaceId: Int, viewTag: Int) {
init(surfaceId, viewTag, uptimeMillis())
}
/** @return false if this Event can *never* be coalesced */
public open fun canCoalesce(): Boolean = true
/**
* Given two events, coalesce them into a single event that will be sent to JS instead of two
* separate events. By default, just chooses the one the is more recent, or `this` if timestamps
* are the same.
*
* Two events will only ever try to be coalesced if they have the same event name, view id, and
* coalescing key.
*/
public open fun coalesce(otherEvent: Event<T>?): Event<T>? =
if (timestampMs >= otherEvent?.timestampMs ?: 0) this else otherEvent
/**
* @return a key used to determine which other events of this type this event can be coalesced
* with. For example, touch move events should only be coalesced within a single gesture so a
* coalescing key there would be the unique gesture id.
*/
public open fun getCoalescingKey(): Short = 0
/**
* Called when the EventDispatcher is done with an event, either because it was dispatched or
* because it was coalesced with another Event.
*/
public open fun onDispose(): Unit = Unit
public fun dispose() {
this.isInitialized = false
onDispose()
}
/** @return the name of this event as registered in JS */
public abstract fun getEventName(): String
public open val eventAnimationDriverMatchSpec: EventAnimationDriverMatchSpec?
get() {
if (eventAnimationDriverMatchSpecCached == null) {
eventAnimationDriverMatchSpecCached =
object : EventAnimationDriverMatchSpec {
override fun match(viewTagRhs: Int, eventNameRhs: String): Boolean {
return viewTag == viewTagRhs && getEventName() == eventNameRhs
}
}
}
return eventAnimationDriverMatchSpecCached
}
/**
* Dispatch this event to JS using the given event emitter. Compatible with old and new renderer.
* Instead of using this or dispatchModern, it is recommended that you simply override
* `getEventData`.
*/
@Deprecated("Prefer to override getEventData instead")
public open fun dispatch(rctEventEmitter: RCTEventEmitter) {
rctEventEmitter.receiveEvent(viewTag, getEventName(), getEventData())
}
/** Can be overridden by classes when no custom logic for dispatching is needed. */
protected open fun getEventData(): WritableMap? = null
/**
* NOTE: This is a transitional method that allows accessing event data from outside the
* sublcasses, but inside the package.
*/
internal fun internal_getEventData(): WritableMap? = getEventData()
protected open fun getEventCategory(): Int = EventCategoryDef.UNSPECIFIED
/**
* NOTE: This is a transitional method that allows accessing event category from outside the
* sublcasses, but inside the package.
*/
internal fun internal_getEventCategory(): Int = getEventCategory()
protected open fun experimental_isSynchronous(): Boolean = false
/**
* NOTE: This is a transitional method that allows accessing event category from outside the
* sublcasses, but inside the package.
*/
internal fun internal_experimental_isSynchronous(): Boolean = experimental_isSynchronous()
/**
* Dispatch this event to JS using a V2 EventEmitter. If surfaceId is not -1 and `getEventData` is
* non-null, this will use the RCTModernEventEmitter API. Otherwise, it falls back to the
* old-style dispatch function. For Event classes that need to do something different, this method
* can always be overridden entirely, but it is not recommended.
*
* This method additionally allows C++ to coalesce events and detect continuous ones for
* concurrent mode (Fabric only).
*
* @see .dispatch
*/
public open fun dispatchModern(rctEventEmitter: RCTModernEventEmitter) {
if (surfaceId != -1) {
rctEventEmitter.receiveEvent(
surfaceId,
viewTag,
getEventName(),
canCoalesce(),
getCoalescingKey().toInt(),
getEventData(),
getEventCategory())
} else {
dispatch(rctEventEmitter)
}
}
public fun interface EventAnimationDriverMatchSpec {
public fun match(viewTagRhs: Int, eventNameRhs: String): Boolean
}
private companion object {
private var uniqueIdCounter = 0
}
}
@@ -63,7 +63,7 @@ internal class FabricEventDispatcher(
for (listener in listeners) {
listener.onEventDispatch(event)
}
if (event.experimental_isSynchronous()) {
if (event.internal_experimental_isSynchronous()) {
dispatchSynchronous(event)
} else {
event.dispatchModern(eventEmitter)
@@ -76,7 +76,7 @@ internal class FabricEventDispatcher(
private fun dispatchSynchronous(event: Event<*>) {
Systrace.beginSection(
Systrace.TRACE_TAG_REACT,
"FabricEventDispatcher.dispatchSynchronous('" + event.eventName + "')")
"FabricEventDispatcher.dispatchSynchronous('" + event.getEventName() + "')")
try {
val fabricUIManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.FABRIC)
@OptIn(UnstableReactNativeAPI::class)
@@ -84,10 +84,10 @@ internal class FabricEventDispatcher(
(fabricUIManager as SynchronousEventReceiver).receiveEvent(
event.surfaceId,
event.viewTag,
event.eventName,
event.getEventName(),
event.canCoalesce(),
event.eventData,
event.eventCategory,
event.internal_getEventData(),
event.internal_getEventCategory(),
true)
} else {
ReactSoftExceptionLogger.logSoftException(
@@ -124,7 +124,8 @@ internal object TouchesHelper {
@JvmStatic
public fun sendTouchEvent(eventEmitter: RCTModernEventEmitter, event: TouchEvent) {
Systrace.beginSection(
Systrace.TRACE_TAG_REACT, "TouchesHelper.sentTouchEventModern(" + event.eventName + ")")
Systrace.TRACE_TAG_REACT,
"TouchesHelper.sentTouchEventModern(" + event.getEventName() + ")")
try {
val type = event.getTouchEventType()
val motionEvent = event.getMotionEvent()
@@ -176,11 +177,11 @@ internal object TouchesHelper {
eventEmitter.receiveEvent(
event.surfaceId,
event.viewTag,
event.eventName,
event.getEventName(),
event.canCoalesce(),
0,
eventData,
event.eventCategory)
event.getEventCategory())
}
} finally {
Systrace.endSection(Systrace.TRACE_TAG_REACT)
@@ -53,7 +53,9 @@ public class ScrollEvent private constructor() : Event<ScrollEvent>() {
scrollViewWidth: Int,
scrollViewHeight: Int,
) {
super.init(surfaceId, viewTag)
val timestampMs = SystemClock.uptimeMillis()
super.init(surfaceId, viewTag, timestampMs)
this.scrollEventType = scrollEventType
this.scrollX = scrollX
this.scrollY = scrollY
@@ -63,7 +65,7 @@ public class ScrollEvent private constructor() : Event<ScrollEvent>() {
this.contentHeight = contentHeight
this.scrollViewWidth = scrollViewWidth
this.scrollViewHeight = scrollViewHeight
this.timestamp = SystemClock.uptimeMillis()
this.timestamp = timestampMs
}
override fun getEventName(): String =
@@ -899,22 +899,22 @@ class NativeAnimatedNodeTraversalTest {
verifyNoMoreInteractions(uiManagerMock)
}
private fun createScrollEvent(tag: Int, value: Double): Event<Event<*>> {
return object : Event<Event<*>>(tag) {
private class TestScrollEvent(private val tag: Int, private val value: Double) :
Event<TestScrollEvent>(tag) {
override fun getEventName(): String {
return "topScroll"
}
override fun getEventName(): String {
return "topScroll"
}
@Override
@Deprecated("Deprecated in Java")
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
rctEventEmitter.receiveEvent(
tag, "topScroll", JavaOnlyMap.of("contentOffset", JavaOnlyMap.of("y", value)))
}
@Deprecated("Deprecated in Java")
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
rctEventEmitter.receiveEvent(
tag, "topScroll", JavaOnlyMap.of("contentOffset", JavaOnlyMap.of("y", value)))
}
}
private fun createScrollEvent(tag: Int, value: Double): Event<*> = TestScrollEvent(tag, value)
@Test
fun testNativeAnimatedEventDoUpdate() {
val viewTag = 1000
@@ -35,8 +35,8 @@ class JSPointerDispatcherTest {
private lateinit var root: ViewGroup
private lateinit var pointerDispatcher: JSPointerDispatcher
class EventWithName(private val eventName: String) : ArgumentMatcher<Event<Event<*>>> {
override fun matches(argument: Event<Event<*>>?): Boolean = argument?.eventName == eventName
class EventWithName(private val eventName: String) : ArgumentMatcher<Event<*>> {
override fun matches(argument: Event<*>?): Boolean = argument?.getEventName() == eventName
override fun toString(): String = "[event with name: $eventName]"
}
@@ -58,7 +58,7 @@ class OnLayoutEventTest {
fun testGetEventName_shouldReturnCorrectEventName() {
val event = OnLayoutEvent.obtain(1, 1, 10, 20, 100, 200)
assertThat(event.eventName).isEqualTo("topLayout")
assertThat(event.getEventName()).isEqualTo("topLayout")
}
@Test