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 @@ -7,38 +7,33 @@

package com.facebook.react.uimanager.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.bridge.ReactNativeJNISoLoader
import com.facebook.react.bridge.WritableNativeMap
import com.facebook.react.uimanager.PixelUtil.pxToDp
import com.facebook.systrace.Systrace

/**
* Class responsible for generating catalyst touch events based on android
* [android.view.MotionEvent].
*/
@DoNotStrip
internal object TouchesHelper {
@JvmField @Deprecated("Not used in New Architecture") val TARGET_KEY: String = "target"

private const val TARGET_SURFACE_KEY = "targetSurface"
private const val CHANGED_TOUCHES_KEY = "changedTouches"
private const val TOUCHES_KEY = "touches"
private const val PAGE_X_KEY = "pageX"
private const val PAGE_Y_KEY = "pageY"
private const val TIMESTAMP_KEY = "timestamp"
private const val POINTER_IDENTIFIER_KEY = "identifier"
init {
ReactNativeJNISoLoader.staticInit()
}

private const val LOCATION_X_KEY = "locationX"
private const val LOCATION_Y_KEY = "locationY"
// Values per pointer in the data passed to nativeCreateTouchPayload.
private const val POINTER_FIELDS = 8

/**
* Creates catalyst pointers array in format that is expected by RCTEventEmitter JS module from
* given {@param event} instance. This method use {@param reactTarget} parameter to set as a
* target view id associated with current gesture.
* Fills the pointer data of [nativeCreateTouchPayload]: for each pointer of the
* [android.view.MotionEvent], in order, pageX, pageY, locationX, locationY, targetSurface,
* target, timestamp and identifier.
*/
private fun createPointersArray(event: TouchEvent): Array<WritableMap?> {
private fun createPointerData(event: TouchEvent): DoubleArray {
val motionEvent = event.getMotionEvent()
val touches = arrayOfNulls<WritableMap>(motionEvent.pointerCount)
val pointerData = DoubleArray(motionEvent.pointerCount * POINTER_FIELDS)

// Calculate the coordinates for the target view.
// The MotionEvent contains the X,Y of the touch in the coordinate space of the root view
Expand All @@ -51,29 +46,27 @@ internal object TouchesHelper {
val targetViewCoordinateY = motionEvent.y - event.viewY

for (index in 0 until motionEvent.pointerCount) {
val touch = Arguments.createMap()
var field = index * POINTER_FIELDS

// pageX,Y values are relative to the RootReactView
// the motionEvent already contains coordinates in that view
touch.putDouble(PAGE_X_KEY, motionEvent.getX(index).pxToDp().toDouble())
touch.putDouble(PAGE_Y_KEY, motionEvent.getY(index).pxToDp().toDouble())
pointerData[field++] = motionEvent.getX(index).pxToDp().toDouble()
pointerData[field++] = motionEvent.getY(index).pxToDp().toDouble()

// locationX,Y values are relative to the target view
// To compute the values for the view, we subtract that views location from the event X,Y
val locationX = motionEvent.getX(index) - targetViewCoordinateX
val locationY = motionEvent.getY(index) - targetViewCoordinateY
touch.putDouble(LOCATION_X_KEY, locationX.pxToDp().toDouble())
touch.putDouble(LOCATION_Y_KEY, locationY.pxToDp().toDouble())
pointerData[field++] = locationX.pxToDp().toDouble()
pointerData[field++] = locationY.pxToDp().toDouble()

touch.putInt(TARGET_SURFACE_KEY, event.surfaceId)
@Suppress("DEPRECATION") touch.putInt(TARGET_KEY, event.viewTag)
touch.putDouble(TIMESTAMP_KEY, event.timestampMs.toDouble())
touch.putDouble(POINTER_IDENTIFIER_KEY, motionEvent.getPointerId(index).toDouble())

touches[index] = touch
pointerData[field++] = event.surfaceId.toDouble()
pointerData[field++] = event.viewTag.toDouble()
pointerData[field++] = event.timestampMs.toDouble()
pointerData[field] = motionEvent.getPointerId(index).toDouble()
}

return touches
return pointerData
}

/**
Expand All @@ -82,8 +75,9 @@ internal object TouchesHelper {
* more efficient manner.
*
* Touches have to be dispatched as separate events for each changed pointer to make JS process
* them correctly. To avoid allocations, we preprocess touch events in Java world and then convert
* them to native before dispatch.
* them correctly. The pointers are collected once into a [DoubleArray], and each event's payload
* is built from it in a single JNI call, instead of putting every value into a native map one by
* one.
*
* @param eventEmitter emitter to dispatch event to
* @param event the touch event to extract data from
Expand All @@ -95,52 +89,44 @@ internal object TouchesHelper {
"TouchesHelper.sentTouchEventModern(" + event.getEventName() + ")",
)
try {
val type = event.getTouchEventType()
val motionEvent = event.getMotionEvent()
var touches = createPointersArray(event)
val changedTouches: Array<WritableMap?>?

when (type) {
val pointerCount = motionEvent.pointerCount
// Pointers are passed to native as bit masks of their indices.
check(pointerCount < Int.SIZE_BITS) { "Too many pointers: $pointerCount" }
val allPointers = (1 shl pointerCount) - 1
val changedPointers: Int
val touchPointers: Int

when (event.getTouchEventType()) {
TouchEventType.START -> {
val newPointerIndex = motionEvent.actionIndex

changedTouches = arrayOf(touches[newPointerIndex]?.copy())
changedPointers = 1 shl motionEvent.actionIndex
touchPointers = allPointers
}
TouchEventType.END -> {
val finishedPointerIndex = motionEvent.actionIndex
changedPointers = 1 shl motionEvent.actionIndex
/*
* Clear finished pointer index for compatibility with W3C touch "end" events, where the
* Leave out the finished pointer for compatibility with W3C touch "end" events, where the
* active touches don't include the set that has just been "ended".
*/
val finishedPointer = touches[finishedPointerIndex]
touches[finishedPointerIndex] = null

changedTouches = arrayOf(finishedPointer)
touchPointers = allPointers and changedPointers.inv()
}
TouchEventType.MOVE -> {
changedTouches = arrayOfNulls(touches.size)
var i = 0
while (i < touches.size) {
changedTouches[i] = touches[i]?.copy()
i++
}
changedPointers = allPointers
touchPointers = allPointers
}
TouchEventType.CANCEL -> {
changedTouches = touches
touches = arrayOfNulls(0)
changedPointers = allPointers
touchPointers = 0
}
}

for (touchData in changedTouches) {
val pointerData = createPointerData(event)
for (pointer in 0 until pointerCount) {
if (changedPointers and (1 shl pointer) == 0) {
continue
}
val eventData =
touchData?.let { td ->
val ed = td.copy()
val changedTouchesArray = getWritableArray(/* copyObjects */ true, changedTouches)
val touchesArray = getWritableArray(/* copyObjects */ true, touches)
ed.putArray(CHANGED_TOUCHES_KEY, changedTouchesArray)
ed.putArray(TOUCHES_KEY, touchesArray)
ed
}
nativeCreateTouchPayload(pointerData, pointer, changedPointers, touchPointers)

eventEmitter.receiveEvent(
event.surfaceId,
Expand All @@ -158,13 +144,20 @@ internal object TouchesHelper {
}
}

private fun getWritableArray(copyObjects: Boolean, objects: Array<WritableMap?>): WritableArray {
val result = Arguments.createArray()
for (obj in objects) {
if (obj != null) {
result.pushMap(if (copyObjects) obj.copy() else obj)
}
}
return result
}
/**
* Creates the payload of a touch event: the fields of pointer [eventPointer] and the
* `changedTouches` and `touches` arrays of the pointers whose bits are set in [changedPointers]
* and [touchPointers].
*
* @param pointerData 8 values for each pointer, in order: pageX, pageY, locationX, locationY,
* targetSurface, target, timestamp and identifier.
*/
@DoNotStrip
@JvmStatic
private external fun nativeCreateTouchPayload(
pointerData: DoubleArray,
eventPointer: Int,
changedPointers: Int,
touchPointers: Int,
): WritableNativeMap
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ add_library(
OnLoad-common.cpp
ReadableNativeArray.cpp
ReadableNativeMap.cpp
TouchesHelper.cpp
TransformHelper.cpp
WritableNativeArray.cpp
WritableNativeMap.cpp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <fbjni/fbjni.h>

#include "InspectorNetworkRequestListener.h"
#include "TouchesHelper.h"
#include "TransformHelper.h"

#ifndef WITH_GLOGINIT
Expand All @@ -34,6 +35,7 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
FLAGS_minloglevel = 0;
#endif
InspectorNetworkRequestListener::registerNatives();
TouchesHelper::registerNatives();
TransformHelper::registerNatives();
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 "TouchesHelper.h"

#include <stdexcept>

#include "WritableNativeArray.h"
#include "WritableNativeMap.h"

using namespace facebook::jni;

namespace facebook::react {

namespace {

// Must match TouchesHelper.POINTER_FIELDS and the order documented on
// TouchesHelper.nativeCreateTouchPayload.
constexpr jint kPointerFields = 8;

folly::dynamic createTouch(const double* fields) {
folly::dynamic touch = folly::dynamic::object();
touch.insert("pageX", fields[0]);
touch.insert("pageY", fields[1]);
touch.insert("locationX", fields[2]);
touch.insert("locationY", fields[3]);
touch.insert("targetSurface", static_cast<int>(fields[4]));
touch.insert("target", static_cast<int>(fields[5]));
touch.insert("timestamp", fields[6]);
touch.insert("identifier", fields[7]);
return touch;
}

local_ref<WritableNativeMap::jhybridobject> createTouchPayload(
alias_ref<jclass> /*unused*/,
alias_ref<JArrayDouble> pointerData,
jint eventPointer,
jint changedPointers,
jint touchPointers) {
auto length = static_cast<jint>(pointerData->size());
jint pointerCount = length / kPointerFields;
// Pointers are selected by bits of the jint masks.
if (length % kPointerFields != 0 || pointerCount >= 32 || eventPointer < 0 ||
eventPointer >= pointerCount) {
throw std::invalid_argument("Invalid touch pointer data");
}
auto values = pointerData->getRegion(0, length);

folly::dynamic changedTouches = folly::dynamic::array();
folly::dynamic touches = folly::dynamic::array();
for (jint index = 0; index < pointerCount; index++) {
const double* fields = values.get() + index * kPointerFields;
if ((changedPointers & (1 << index)) != 0) {
changedTouches.push_back(createTouch(fields));
}
if ((touchPointers & (1 << index)) != 0) {
touches.push_back(createTouch(fields));
}
}

auto jPayload = WritableNativeMap::newObjectJavaArgs();
auto* payload = jPayload->cthis();
const double* fields = values.get() + eventPointer * kPointerFields;
payload->putDouble("pageX", fields[0]);
payload->putDouble("pageY", fields[1]);
payload->putDouble("locationX", fields[2]);
payload->putDouble("locationY", fields[3]);
payload->putInt("targetSurface", static_cast<int>(fields[4]));
payload->putInt("target", static_cast<int>(fields[5]));
payload->putDouble("timestamp", fields[6]);
payload->putDouble("identifier", fields[7]);
WritableNativeArray changedTouchesArray(std::move(changedTouches));
payload->putNativeArray("changedTouches", &changedTouchesArray);
WritableNativeArray touchesArray(std::move(touches));
payload->putNativeArray("touches", &touchesArray);
return jPayload;
}

} // namespace

void TouchesHelper::registerNatives() {
javaClassLocal()->registerNatives({
makeNativeMethod("nativeCreateTouchPayload", createTouchPayload),
});
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -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 <fbjni/fbjni.h>

namespace facebook::react {

class TouchesHelper : public jni::JavaClass<TouchesHelper> {
public:
static auto constexpr *kJavaDescriptor = "Lcom/facebook/react/uimanager/events/TouchesHelper;";

static void registerNatives();
};

} // namespace facebook::react
Loading
Loading