From b23960ad99b3d20336464a4f3cc8a9da211fb361 Mon Sep 17 00:00:00 2001 From: Ruslan Shestopalyuk Date: Tue, 9 Apr 2024 07:01:54 -0700 Subject: [PATCH] Migrate TouchEvent to Kotlin (#43982) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43982 ## Changelog: [Internal] - As in the title. Reviewed By: javache Differential Revision: D55877979 fbshipit-source-id: 63ca9b403509e08632ad1a3c0aa866f6df3f423b --- .../ReactAndroid/api/ReactAndroid.api | 21 +- .../react/uimanager/events/TouchEvent.java | 250 ------------------ .../react/uimanager/events/TouchEvent.kt | 207 +++++++++++++++ 3 files changed, 220 insertions(+), 258 deletions(-) delete mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index 6d849d11d3ad67..8a4e10def6ee8a 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -5646,23 +5646,28 @@ public abstract interface class com/facebook/react/uimanager/events/RCTModernEve public abstract fun receiveTouches (Lcom/facebook/react/uimanager/events/TouchEvent;)V } -public class com/facebook/react/uimanager/events/TouchEvent : com/facebook/react/uimanager/events/Event { +public final class com/facebook/react/uimanager/events/TouchEvent : com/facebook/react/uimanager/events/Event { + public static final field Companion Lcom/facebook/react/uimanager/events/TouchEvent$Companion; public static final field UNSET J public fun canCoalesce ()Z public fun dispatch (Lcom/facebook/react/uimanager/events/RCTEventEmitter;)V public fun dispatchModern (Lcom/facebook/react/uimanager/events/RCTModernEventEmitter;)V public fun getCoalescingKey ()S - protected fun getEventCategory ()I public fun getEventName ()Ljava/lang/String; - public fun getMotionEvent ()Landroid/view/MotionEvent; - public fun getTouchEventType ()Lcom/facebook/react/uimanager/events/TouchEventType; - public fun getViewX ()F - public fun getViewY ()F - public static fun obtain (IILcom/facebook/react/uimanager/events/TouchEventType;Landroid/view/MotionEvent;JFFLcom/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper;)Lcom/facebook/react/uimanager/events/TouchEvent; - public static fun obtain (ILcom/facebook/react/uimanager/events/TouchEventType;Landroid/view/MotionEvent;JFFLcom/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper;)Lcom/facebook/react/uimanager/events/TouchEvent; + public final fun getMotionEvent ()Landroid/view/MotionEvent; + public final fun getTouchEventType ()Lcom/facebook/react/uimanager/events/TouchEventType; + public final fun getViewX ()F + public final fun getViewY ()F + public static final fun obtain (IILcom/facebook/react/uimanager/events/TouchEventType;Landroid/view/MotionEvent;JFFLcom/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper;)Lcom/facebook/react/uimanager/events/TouchEvent; + public static final fun obtain (ILcom/facebook/react/uimanager/events/TouchEventType;Landroid/view/MotionEvent;JFFLcom/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper;)Lcom/facebook/react/uimanager/events/TouchEvent; public fun onDispose ()V } +public final class com/facebook/react/uimanager/events/TouchEvent$Companion { + public final fun obtain (IILcom/facebook/react/uimanager/events/TouchEventType;Landroid/view/MotionEvent;JFFLcom/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper;)Lcom/facebook/react/uimanager/events/TouchEvent; + public final fun obtain (ILcom/facebook/react/uimanager/events/TouchEventType;Landroid/view/MotionEvent;JFFLcom/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper;)Lcom/facebook/react/uimanager/events/TouchEvent; +} + public class com/facebook/react/uimanager/events/TouchEventCoalescingKeyHelper { public fun ()V public fun addCoalescingKey (J)V diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java deleted file mode 100644 index bc32cd4b768ca2..00000000000000 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.java +++ /dev/null @@ -1,250 +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 android.view.MotionEvent; -import androidx.annotation.Nullable; -import androidx.core.util.Pools; -import com.facebook.infer.annotation.Assertions; -import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.bridge.ReactSoftExceptionLogger; -import com.facebook.react.bridge.SoftAssertions; - -/** - * An event representing the start, end or movement of a touch. Corresponds to a single {@link - * android.view.MotionEvent}. - * - *

TouchEvent coalescing can happen for move events if two move events have the same target view - * and coalescing key. See {@link TouchEventCoalescingKeyHelper} for more information about how - * these coalescing keys are determined. - */ -@Nullsafe(Nullsafe.Mode.LOCAL) -public class TouchEvent extends Event { - private static final String TAG = TouchEvent.class.getSimpleName(); - - private static final int TOUCH_EVENTS_POOL_SIZE = 3; - - private static final Pools.SynchronizedPool EVENTS_POOL = - new Pools.SynchronizedPool<>(TOUCH_EVENTS_POOL_SIZE); - - public static final long UNSET = Long.MIN_VALUE; - - @Deprecated - public static TouchEvent obtain( - int viewTag, - TouchEventType touchEventType, - MotionEvent motionEventToCopy, - long gestureStartTime, - float viewX, - float viewY, - TouchEventCoalescingKeyHelper touchEventCoalescingKeyHelper) { - return obtain( - -1, - viewTag, - touchEventType, - Assertions.assertNotNull(motionEventToCopy), - gestureStartTime, - viewX, - viewY, - touchEventCoalescingKeyHelper); - } - - public static TouchEvent obtain( - int surfaceId, - int viewTag, - TouchEventType touchEventType, - MotionEvent motionEventToCopy, - long gestureStartTime, - float viewX, - float viewY, - TouchEventCoalescingKeyHelper touchEventCoalescingKeyHelper) { - TouchEvent event = EVENTS_POOL.acquire(); - if (event == null) { - event = new TouchEvent(); - } - event.init( - surfaceId, - viewTag, - touchEventType, - Assertions.assertNotNull(motionEventToCopy), - gestureStartTime, - viewX, - viewY, - touchEventCoalescingKeyHelper); - return event; - } - - private @Nullable MotionEvent mMotionEvent; - private @Nullable TouchEventType mTouchEventType; - private short mCoalescingKey; - - // Coordinates in the ViewTag coordinate space - private float mViewX; - private float mViewY; - - private TouchEvent() {} - - private void init( - int surfaceId, - int viewTag, - TouchEventType touchEventType, - MotionEvent motionEventToCopy, - long gestureStartTime, - float viewX, - float viewY, - TouchEventCoalescingKeyHelper touchEventCoalescingKeyHelper) { - super.init(surfaceId, viewTag, motionEventToCopy.getEventTime()); - - SoftAssertions.assertCondition( - gestureStartTime != UNSET, "Gesture start time must be initialized"); - short coalescingKey = 0; - int action = (motionEventToCopy.getAction() & MotionEvent.ACTION_MASK); - switch (action) { - case MotionEvent.ACTION_DOWN: - touchEventCoalescingKeyHelper.addCoalescingKey(gestureStartTime); - break; - case MotionEvent.ACTION_UP: - touchEventCoalescingKeyHelper.removeCoalescingKey(gestureStartTime); - break; - case MotionEvent.ACTION_POINTER_DOWN: - case MotionEvent.ACTION_POINTER_UP: - touchEventCoalescingKeyHelper.incrementCoalescingKey(gestureStartTime); - break; - case MotionEvent.ACTION_MOVE: - coalescingKey = touchEventCoalescingKeyHelper.getCoalescingKey(gestureStartTime); - break; - case MotionEvent.ACTION_CANCEL: - touchEventCoalescingKeyHelper.removeCoalescingKey(gestureStartTime); - break; - default: - throw new RuntimeException("Unhandled MotionEvent action: " + action); - } - mTouchEventType = touchEventType; - mMotionEvent = MotionEvent.obtain(motionEventToCopy); - mCoalescingKey = coalescingKey; - mViewX = viewX; - mViewY = viewY; - } - - @Override - public void onDispose() { - MotionEvent motionEvent = mMotionEvent; - mMotionEvent = null; - if (motionEvent != null) { - motionEvent.recycle(); - } - - // Either `this` is in the event pool, or motionEvent - // is null. It is in theory not possible for a TouchEvent to - // be in the EVENTS_POOL but for motionEvent to be null. However, - // out of an abundance of caution and to avoid memory leaks or - // other crashes at all costs, we attempt to release here and log - // a soft exception here if release throws an IllegalStateException - // due to `this` being over-released. This may indicate that there is - // a logic error in our events system or pooling mechanism. - try { - EVENTS_POOL.release(this); - } catch (IllegalStateException e) { - ReactSoftExceptionLogger.logSoftException(TAG, e); - } - } - - @Override - public String getEventName() { - return TouchEventType.getJSEventName(Assertions.assertNotNull(mTouchEventType)); - } - - @Override - public boolean canCoalesce() { - // We can coalesce move events but not start/end events. Coalescing move events should probably - // append historical move data like MotionEvent batching does. This is left as an exercise for - // the reader. - switch (Assertions.assertNotNull(mTouchEventType)) { - case START: - case END: - case CANCEL: - return false; - case MOVE: - return true; - default: - throw new RuntimeException("Unknown touch event type: " + mTouchEventType); - } - } - - @Override - public short getCoalescingKey() { - return mCoalescingKey; - } - - @Override - public void dispatch(RCTEventEmitter rctEventEmitter) { - if (verifyMotionEvent()) { - TouchesHelper.sendTouchesLegacy(rctEventEmitter, this); - } - } - - @Override - public void dispatchModern(RCTModernEventEmitter rctEventEmitter) { - if (verifyMotionEvent()) { - // TouchesHelper.sendTouchEvent can be inlined here post Fabric rollout - // For now, we go via the event emitter, which will decide whether the legacy or modern - // event path is required - rctEventEmitter.receiveTouches(this); - } - } - - @Override - protected int getEventCategory() { - TouchEventType type = mTouchEventType; - if (type == null) { - return EventCategoryDef.UNSPECIFIED; - } - - switch (type) { - case START: - return EventCategoryDef.CONTINUOUS_START; - case END: - case CANCEL: - return EventCategoryDef.CONTINUOUS_END; - case MOVE: - return EventCategoryDef.CONTINUOUS; - } - - // Something something smart compiler... - return super.getEventCategory(); - } - - public MotionEvent getMotionEvent() { - Assertions.assertNotNull(mMotionEvent); - return mMotionEvent; - } - - private boolean verifyMotionEvent() { - if (mMotionEvent == null) { - ReactSoftExceptionLogger.logSoftException( - TAG, - new IllegalStateException( - "Cannot dispatch a TouchEvent that has no MotionEvent; the TouchEvent has been" - + " recycled")); - return false; - } - return true; - } - - public TouchEventType getTouchEventType() { - return Assertions.assertNotNull(mTouchEventType); - } - - public float getViewX() { - return mViewX; - } - - public float getViewY() { - return mViewY; - } -} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt new file mode 100644 index 00000000000000..a62acf4be2b42c --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/TouchEvent.kt @@ -0,0 +1,207 @@ +/* + * 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 android.view.MotionEvent +import androidx.core.util.Pools.SynchronizedPool +import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.ReactSoftExceptionLogger +import com.facebook.react.bridge.SoftAssertions +import com.facebook.react.uimanager.events.TouchEventType.Companion.getJSEventName + +/** + * An event representing the start, end or movement of a touch. Corresponds to a single [ ]. + * + * TouchEvent coalescing can happen for move events if two move events have the same target view and + * coalescing key. See [TouchEventCoalescingKeyHelper] for more information about how these + * coalescing keys are determined. + */ +public class TouchEvent private constructor() : Event() { + private var motionEvent: MotionEvent? = null + private var touchEventType: TouchEventType? = null + private var coalescingKey: Short = 0 + + // Coordinates in the ViewTag coordinate space + public var viewX: Float = 0f + private set + + public var viewY: Float = 0f + private set + + public fun getMotionEvent(): MotionEvent = Assertions.assertNotNull(motionEvent) + + public fun getTouchEventType(): TouchEventType = Assertions.assertNotNull(touchEventType) + + private fun init( + surfaceId: Int, + viewTag: Int, + touchEventType: TouchEventType?, + motionEventToCopy: MotionEvent, + gestureStartTime: Long, + viewX: Float, + viewY: Float, + touchEventCoalescingKeyHelper: TouchEventCoalescingKeyHelper + ) { + super.init(surfaceId, viewTag, motionEventToCopy.eventTime) + SoftAssertions.assertCondition( + gestureStartTime != UNSET, "Gesture start time must be initialized") + var coalescingKey: Short = 0 + val action = motionEventToCopy.action and MotionEvent.ACTION_MASK + when (action) { + MotionEvent.ACTION_DOWN -> touchEventCoalescingKeyHelper.addCoalescingKey(gestureStartTime) + MotionEvent.ACTION_UP -> touchEventCoalescingKeyHelper.removeCoalescingKey(gestureStartTime) + MotionEvent.ACTION_POINTER_DOWN, + MotionEvent.ACTION_POINTER_UP -> + touchEventCoalescingKeyHelper.incrementCoalescingKey(gestureStartTime) + MotionEvent.ACTION_MOVE -> + coalescingKey = touchEventCoalescingKeyHelper.getCoalescingKey(gestureStartTime) + MotionEvent.ACTION_CANCEL -> + touchEventCoalescingKeyHelper.removeCoalescingKey(gestureStartTime) + else -> throw RuntimeException("Unhandled MotionEvent action: $action") + } + + motionEvent = MotionEvent.obtain(motionEventToCopy) + + this.touchEventType = touchEventType + this.coalescingKey = coalescingKey + this.viewX = viewX + this.viewY = viewY + } + + override fun onDispose() { + motionEvent?.recycle() + motionEvent = null + + // Either `this` is in the event pool, or motionEvent + // is null. It is in theory not possible for a TouchEvent to + // be in the EVENTS_POOL but for motionEvent to be null. However, + // out of an abundance of caution and to avoid memory leaks or + // other crashes at all costs, we attempt to release here and log + // a soft exception here if release throws an IllegalStateException + // due to `this` being over-released. This may indicate that there is + // a logic error in our events system or pooling mechanism. + try { + EVENTS_POOL.release(this) + } catch (e: IllegalStateException) { + ReactSoftExceptionLogger.logSoftException(TAG, e) + } + } + + override fun getEventName(): String = getJSEventName(Assertions.assertNotNull(touchEventType)) + + // We can coalesce move events but not start/end events. Coalescing move events should probably + // append historical move data like MotionEvent batching does. This is left as an exercise for + // the reader. + override fun canCoalesce(): Boolean = + when (Assertions.assertNotNull(touchEventType)) { + TouchEventType.START, + TouchEventType.END, + TouchEventType.CANCEL -> false + TouchEventType.MOVE -> true + else -> throw RuntimeException("Unknown touch event type: $touchEventType") + } + + override fun getCoalescingKey(): Short = coalescingKey + + override fun dispatch(rctEventEmitter: RCTEventEmitter) { + if (verifyMotionEvent()) { + TouchesHelper.sendTouchesLegacy(rctEventEmitter, this) + } + } + + override fun dispatchModern(rctEventEmitter: RCTModernEventEmitter) { + if (verifyMotionEvent()) { + // TouchesHelper.sendTouchEvent can be inlined here post Fabric rollout + // For now, we go via the event emitter, which will decide whether the legacy or modern + // event path is required + rctEventEmitter.receiveTouches(this) + } + } + + protected override fun getEventCategory(): Int { + val type = touchEventType ?: return EventCategoryDef.UNSPECIFIED + return when (type) { + TouchEventType.START -> EventCategoryDef.CONTINUOUS_START + TouchEventType.END, + TouchEventType.CANCEL -> EventCategoryDef.CONTINUOUS_END + TouchEventType.MOVE -> EventCategoryDef.CONTINUOUS + } + } + + private fun verifyMotionEvent(): Boolean { + if (motionEvent == null) { + ReactSoftExceptionLogger.logSoftException( + TAG, + IllegalStateException( + "Cannot dispatch a TouchEvent that has no MotionEvent; the TouchEvent has been" + + " recycled")) + return false + } + return true + } + + public companion object { + private val TAG = TouchEvent::class.java.simpleName + private const val TOUCH_EVENTS_POOL_SIZE = 3 + private val EVENTS_POOL = SynchronizedPool(TOUCH_EVENTS_POOL_SIZE) + public const val UNSET: Long = Long.MIN_VALUE + + @Deprecated( + "Please use the other overload of the obtain method, which explicitly provides surfaceId", + ReplaceWith("obtain(surfaceId, ...)")) + @JvmStatic + public fun obtain( + viewTag: Int, + touchEventType: TouchEventType?, + motionEventToCopy: MotionEvent?, + gestureStartTime: Long, + viewX: Float, + viewY: Float, + touchEventCoalescingKeyHelper: TouchEventCoalescingKeyHelper + ): TouchEvent { + return obtain( + -1, + viewTag, + touchEventType, + Assertions.assertNotNull(motionEventToCopy), + gestureStartTime, + viewX, + viewY, + touchEventCoalescingKeyHelper) + } + + @JvmStatic + public fun obtain( + surfaceId: Int, + viewTag: Int, + touchEventType: TouchEventType?, + motionEventToCopy: MotionEvent?, + gestureStartTime: Long, + viewX: Float, + viewY: Float, + touchEventCoalescingKeyHelper: TouchEventCoalescingKeyHelper + ): TouchEvent { + var event = EVENTS_POOL.acquire() + if (event == null) { + event = TouchEvent() + } + event.init( + surfaceId, + viewTag, + touchEventType, + Assertions.assertNotNull(motionEventToCopy), + gestureStartTime, + viewX, + viewY, + touchEventCoalescingKeyHelper) + return event + } + } +}