mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-02 22:41:18 +08:00
Summary: **Summary:** There was a bug with RN.Dimensions returning incorrect window dimensions. In certain cases when device was in portrait, window dimensions reported landscape dimensions and vice versa. This happened because in certain scenarios, after device orientation changed, dimensions update event from ReactRootView had incorrect dimensions. Was able to reproduce this when device was rotated during app launch. After rotation global layout listener callback gets invoked. Inside the callback current and previous orientations are compared. When a change is detected, orientation and dimension change events are sent to JS. It is assumed, when orientation changes, new dimensions are available immediately. This is not the case for window dimensions as they are retrieved from resources object which gets updated asynchronously after orientation change. In cases when app is doing a lot of work on the main thread, like app startup, it takes more time to update the resources object. And when orientation change is detected in global layout, resources object is not updated with new dimensions yet. This causes dimensions update to be sent to JS with old window dimensions. Global layout listener callback does get invoked a second time when resources object is finally updated with new dimensions, but since orientation no longer changes, no event is sent to JS. Fixed this by separating dimensions update from orientation update. Now RN keeps track of previous window and screen dimension values. When a change is detected, an event is sent to JS with updated dimensions. This ensures that whenever dimensions change, JS gets the updated values. This has a side effect of sending dimension update twice in some cases. One example is the case above where window dimensions take time to update, but screen dimensions are updated immediately. This will cause two events to be sent to JS. One for window dimensions and one for screen dimensions update. Other change is that initial value for both window and screen fields is empty. Which results in first change to trigger an event. Previously initial orientation value was 0 which meant when app started in normal portrait orientation, first layout did not trigger a dimension update event. Now even first layout sends the event. This should not be an issue as it is to make sure dimensions in JS side are correct. **Testing:** Verified with a sample app that correct dimensions are available when app launches. Verified that after orientation dimensions are updated. Verified that in the scenario described above where window dimensions are updated later, we get correct dimension values in JS. We have incorporated this fix into our app and have been testing it internally. Ats Jenk Microsoft Corp. <!-- Thank you for sending the PR! If you changed any code, please provide us with clear instructions on how you verified your changes work. In other words, a test plan is *required*. Bonus points for screenshots and videos! Please read the Contribution Guidelines at https://github.com/facebook/react-native/blob/master/CONTRIBUTING.md to learn more about contributing to React Native. Happy contributing! --> Closes https://github.com/facebook/react-native/pull/15181 Differential Revision: D5552195 Pulled By: shergin fbshipit-source-id: d1f190cb960090468886ff56cda58cac296745db
493 lines
18 KiB
Java
493 lines
18 KiB
Java
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
package com.facebook.react;
|
|
|
|
import javax.annotation.Nullable;
|
|
|
|
import android.content.Context;
|
|
import android.graphics.Rect;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.util.AttributeSet;
|
|
import android.util.DisplayMetrics;
|
|
import android.view.MotionEvent;
|
|
import android.view.Surface;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewTreeObserver;
|
|
import android.view.WindowManager;
|
|
|
|
import com.facebook.common.logging.FLog;
|
|
import com.facebook.infer.annotation.Assertions;
|
|
import com.facebook.react.bridge.Arguments;
|
|
import com.facebook.react.bridge.CatalystInstance;
|
|
import com.facebook.react.bridge.ReactContext;
|
|
import com.facebook.react.bridge.UiThreadUtil;
|
|
import com.facebook.react.bridge.WritableMap;
|
|
import com.facebook.react.bridge.WritableNativeMap;
|
|
import com.facebook.react.common.ReactConstants;
|
|
import com.facebook.react.common.annotations.VisibleForTesting;
|
|
import com.facebook.react.modules.appregistry.AppRegistry;
|
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
import com.facebook.react.modules.deviceinfo.DeviceInfoModule;
|
|
import com.facebook.react.uimanager.DisplayMetricsHolder;
|
|
import com.facebook.react.uimanager.JSTouchDispatcher;
|
|
import com.facebook.react.uimanager.PixelUtil;
|
|
import com.facebook.react.uimanager.RootView;
|
|
import com.facebook.react.uimanager.SizeMonitoringFrameLayout;
|
|
import com.facebook.react.uimanager.UIManagerModule;
|
|
import com.facebook.react.uimanager.events.EventDispatcher;
|
|
import com.facebook.systrace.Systrace;
|
|
|
|
import static com.facebook.systrace.Systrace.TRACE_TAG_REACT_JAVA_BRIDGE;
|
|
|
|
/**
|
|
* Default root view for catalyst apps. Provides the ability to listen for size changes so that a UI
|
|
* manager can re-layout its elements.
|
|
* It delegates handling touch events for itself and child views and sending those events to JS by
|
|
* using JSTouchDispatcher.
|
|
* This view is overriding {@link ViewGroup#onInterceptTouchEvent} method in order to be notified
|
|
* about the events for all of its children and it's also overriding
|
|
* {@link ViewGroup#requestDisallowInterceptTouchEvent} to make sure that
|
|
* {@link ViewGroup#onInterceptTouchEvent} will get events even when some child view start
|
|
* intercepting it. In case when no child view is interested in handling some particular
|
|
* touch event this view's {@link View#onTouchEvent} will still return true in order to be notified
|
|
* about all subsequent touch events related to that gesture (in case when JS code want to handle
|
|
* that gesture).
|
|
*/
|
|
public class ReactRootView extends SizeMonitoringFrameLayout implements RootView {
|
|
|
|
/**
|
|
* Listener interface for react root view events
|
|
*/
|
|
public interface ReactRootViewEventListener {
|
|
/**
|
|
* Called when the react context is attached to a ReactRootView.
|
|
*/
|
|
void onAttachedToReactInstance(ReactRootView rootView);
|
|
}
|
|
|
|
private @Nullable ReactInstanceManager mReactInstanceManager;
|
|
private @Nullable String mJSModuleName;
|
|
private @Nullable Bundle mAppProperties;
|
|
private @Nullable CustomGlobalLayoutListener mCustomGlobalLayoutListener;
|
|
private @Nullable ReactRootViewEventListener mRootViewEventListener;
|
|
private int mRootViewTag;
|
|
private boolean mIsAttachedToInstance;
|
|
private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this);
|
|
|
|
public ReactRootView(Context context) {
|
|
super(context);
|
|
}
|
|
|
|
public ReactRootView(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
}
|
|
|
|
public ReactRootView(Context context, AttributeSet attrs, int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "ReactRootView.onMeasure");
|
|
try {
|
|
setMeasuredDimension(
|
|
MeasureSpec.getSize(widthMeasureSpec),
|
|
MeasureSpec.getSize(heightMeasureSpec));
|
|
|
|
// Check if we were waiting for onMeasure to attach the root view.
|
|
if (mReactInstanceManager != null && !mIsAttachedToInstance) {
|
|
attachToReactInstanceManager();
|
|
}
|
|
} finally {
|
|
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onChildStartedNativeGesture(MotionEvent androidEvent) {
|
|
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
|
mReactInstanceManager.getCurrentReactContext() == null) {
|
|
FLog.w(
|
|
ReactConstants.TAG,
|
|
"Unable to dispatch touch to JS as the catalyst instance has not been attached");
|
|
return;
|
|
}
|
|
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
|
EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
|
|
.getEventDispatcher();
|
|
mJSTouchDispatcher.onChildStartedNativeGesture(androidEvent, eventDispatcher);
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
dispatchJSTouchEvent(ev);
|
|
return super.onInterceptTouchEvent(ev);
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
|
dispatchJSTouchEvent(ev);
|
|
super.onTouchEvent(ev);
|
|
// In case when there is no children interested in handling touch event, we return true from
|
|
// the root view in order to receive subsequent events related to that gesture
|
|
return true;
|
|
}
|
|
|
|
private void dispatchJSTouchEvent(MotionEvent event) {
|
|
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
|
mReactInstanceManager.getCurrentReactContext() == null) {
|
|
FLog.w(
|
|
ReactConstants.TAG,
|
|
"Unable to dispatch touch to JS as the catalyst instance has not been attached");
|
|
return;
|
|
}
|
|
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
|
EventDispatcher eventDispatcher = reactContext.getNativeModule(UIManagerModule.class)
|
|
.getEventDispatcher();
|
|
mJSTouchDispatcher.handleTouchEvent(event, eventDispatcher);
|
|
}
|
|
|
|
@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.
|
|
if (getParent() != null) {
|
|
getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
// No-op since UIManagerModule handles actually laying out children.
|
|
}
|
|
|
|
@Override
|
|
protected void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
if (mIsAttachedToInstance) {
|
|
getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
if (mIsAttachedToInstance) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
|
getViewTreeObserver().removeOnGlobalLayoutListener(getCustomGlobalLayoutListener());
|
|
} else {
|
|
getViewTreeObserver().removeGlobalOnLayoutListener(getCustomGlobalLayoutListener());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@see #startReactApplication(ReactInstanceManager, String, android.os.Bundle)}
|
|
*/
|
|
public void startReactApplication(ReactInstanceManager reactInstanceManager, String moduleName) {
|
|
startReactApplication(reactInstanceManager, moduleName, null);
|
|
}
|
|
|
|
/**
|
|
* Schedule rendering of the react component rendered by the JS application from the given JS
|
|
* module (@{param moduleName}) using provided {@param reactInstanceManager} to attach to the
|
|
* JS context of that manager. Extra parameter {@param launchOptions} can be used to pass initial
|
|
* properties for the react component.
|
|
*/
|
|
public void startReactApplication(
|
|
ReactInstanceManager reactInstanceManager,
|
|
String moduleName,
|
|
@Nullable Bundle initialProperties) {
|
|
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "startReactApplication");
|
|
try {
|
|
UiThreadUtil.assertOnUiThread();
|
|
|
|
// TODO(6788889): Use POJO instead of bundle here, apparently we can't just use WritableMap
|
|
// here as it may be deallocated in native after passing via JNI bridge, but we want to reuse
|
|
// it in the case of re-creating the catalyst instance
|
|
Assertions.assertCondition(
|
|
mReactInstanceManager == null,
|
|
"This root view has already been attached to a catalyst instance manager");
|
|
|
|
mReactInstanceManager = reactInstanceManager;
|
|
mJSModuleName = moduleName;
|
|
mAppProperties = initialProperties;
|
|
|
|
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
|
|
mReactInstanceManager.createReactContextInBackground();
|
|
}
|
|
|
|
attachToReactInstanceManager();
|
|
} finally {
|
|
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unmount the react application at this root view, reclaiming any JS memory associated with that
|
|
* application. If {@link #startReactApplication} is called, this method must be called before the
|
|
* ReactRootView is garbage collected (typically in your Activity's onDestroy, or in your
|
|
* Fragment's onDestroyView).
|
|
*/
|
|
public void unmountReactApplication() {
|
|
if (mReactInstanceManager != null && mIsAttachedToInstance) {
|
|
mReactInstanceManager.detachRootView(this);
|
|
mIsAttachedToInstance = false;
|
|
}
|
|
}
|
|
|
|
public void onAttachedToReactInstance() {
|
|
if (mRootViewEventListener != null) {
|
|
mRootViewEventListener.onAttachedToReactInstance(this);
|
|
}
|
|
}
|
|
|
|
public void setEventListener(ReactRootViewEventListener eventListener) {
|
|
mRootViewEventListener = eventListener;
|
|
}
|
|
|
|
/* package */ String getJSModuleName() {
|
|
return Assertions.assertNotNull(mJSModuleName);
|
|
}
|
|
|
|
public @Nullable Bundle getAppProperties() {
|
|
return mAppProperties;
|
|
}
|
|
|
|
public void setAppProperties(@Nullable Bundle appProperties) {
|
|
UiThreadUtil.assertOnUiThread();
|
|
mAppProperties = appProperties;
|
|
runApplication();
|
|
}
|
|
|
|
/**
|
|
* Calls into JS to start the React application. Can be called multiple times with the
|
|
* same rootTag, which will re-render the application from the root.
|
|
*/
|
|
/* package */ void runApplication() {
|
|
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "ReactRootView.runApplication");
|
|
try {
|
|
if (mReactInstanceManager == null || !mIsAttachedToInstance) {
|
|
return;
|
|
}
|
|
|
|
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
|
if (reactContext == null) {
|
|
return;
|
|
}
|
|
|
|
CatalystInstance catalystInstance = reactContext.getCatalystInstance();
|
|
|
|
WritableNativeMap appParams = new WritableNativeMap();
|
|
appParams.putDouble("rootTag", getRootViewTag());
|
|
@Nullable Bundle appProperties = getAppProperties();
|
|
if (appProperties != null) {
|
|
appParams.putMap("initialProps", Arguments.fromBundle(appProperties));
|
|
}
|
|
|
|
String jsAppModuleName = getJSModuleName();
|
|
catalystInstance.getJSModule(AppRegistry.class).runApplication(jsAppModuleName, appParams);
|
|
} finally {
|
|
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is used by unit test to setup mIsAttachedToWindow flags, that will let this
|
|
* view to be properly attached to catalyst instance by startReactApplication call
|
|
*/
|
|
@VisibleForTesting
|
|
/* package */ void simulateAttachForTesting() {
|
|
mIsAttachedToInstance = true;
|
|
}
|
|
|
|
private CustomGlobalLayoutListener getCustomGlobalLayoutListener() {
|
|
if (mCustomGlobalLayoutListener == null) {
|
|
mCustomGlobalLayoutListener = new CustomGlobalLayoutListener();
|
|
}
|
|
return mCustomGlobalLayoutListener;
|
|
}
|
|
|
|
private void attachToReactInstanceManager() {
|
|
Systrace.beginSection(TRACE_TAG_REACT_JAVA_BRIDGE, "attachToReactInstanceManager");
|
|
try {
|
|
if (mIsAttachedToInstance) {
|
|
return;
|
|
}
|
|
|
|
mIsAttachedToInstance = true;
|
|
Assertions.assertNotNull(mReactInstanceManager).attachRootView(this);
|
|
getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener());
|
|
} finally {
|
|
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void finalize() throws Throwable {
|
|
super.finalize();
|
|
Assertions.assertCondition(
|
|
!mIsAttachedToInstance,
|
|
"The application this ReactRootView was rendering was not unmounted before the " +
|
|
"ReactRootView was garbage collected. This usually means that your application is " +
|
|
"leaking large amounts of memory. To solve this, make sure to call " +
|
|
"ReactRootView#unmountReactApplication in the onDestroy() of your hosting Activity or in " +
|
|
"the onDestroyView() of your hosting Fragment.");
|
|
}
|
|
|
|
public int getRootViewTag() {
|
|
return mRootViewTag;
|
|
}
|
|
|
|
public void setRootViewTag(int rootViewTag) {
|
|
mRootViewTag = rootViewTag;
|
|
}
|
|
|
|
private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
|
|
private final Rect mVisibleViewArea;
|
|
private final int mMinKeyboardHeightDetected;
|
|
|
|
private int mKeyboardHeight = 0;
|
|
private int mDeviceRotation = 0;
|
|
private DisplayMetrics mWindowMetrics = new DisplayMetrics();
|
|
private DisplayMetrics mScreenMetrics = new DisplayMetrics();
|
|
|
|
/* package */ CustomGlobalLayoutListener() {
|
|
DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext());
|
|
mVisibleViewArea = new Rect();
|
|
mMinKeyboardHeightDetected = (int) PixelUtil.toPixelFromDIP(60);
|
|
}
|
|
|
|
@Override
|
|
public void onGlobalLayout() {
|
|
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
|
mReactInstanceManager.getCurrentReactContext() == null) {
|
|
return;
|
|
}
|
|
checkForKeyboardEvents();
|
|
checkForDeviceOrientationChanges();
|
|
checkForDeviceDimensionsChanges();
|
|
}
|
|
|
|
private void checkForKeyboardEvents() {
|
|
getRootView().getWindowVisibleDisplayFrame(mVisibleViewArea);
|
|
final int heightDiff =
|
|
DisplayMetricsHolder.getWindowDisplayMetrics().heightPixels - mVisibleViewArea.bottom;
|
|
if (mKeyboardHeight != heightDiff && heightDiff > mMinKeyboardHeightDetected) {
|
|
// keyboard is now showing, or the keyboard height has changed
|
|
mKeyboardHeight = heightDiff;
|
|
WritableMap params = Arguments.createMap();
|
|
WritableMap coordinates = Arguments.createMap();
|
|
coordinates.putDouble("screenY", PixelUtil.toDIPFromPixel(mVisibleViewArea.bottom));
|
|
coordinates.putDouble("screenX", PixelUtil.toDIPFromPixel(mVisibleViewArea.left));
|
|
coordinates.putDouble("width", PixelUtil.toDIPFromPixel(mVisibleViewArea.width()));
|
|
coordinates.putDouble("height", PixelUtil.toDIPFromPixel(mKeyboardHeight));
|
|
params.putMap("endCoordinates", coordinates);
|
|
sendEvent("keyboardDidShow", params);
|
|
} else if (mKeyboardHeight != 0 && heightDiff <= mMinKeyboardHeightDetected) {
|
|
// keyboard is now hidden
|
|
mKeyboardHeight = 0;
|
|
sendEvent("keyboardDidHide", null);
|
|
}
|
|
}
|
|
|
|
private void checkForDeviceOrientationChanges() {
|
|
final int rotation =
|
|
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
|
|
.getDefaultDisplay().getRotation();
|
|
if (mDeviceRotation == rotation) {
|
|
return;
|
|
}
|
|
mDeviceRotation = rotation;
|
|
emitOrientationChanged(rotation);
|
|
}
|
|
|
|
private void checkForDeviceDimensionsChanges() {
|
|
// Get current display metrics.
|
|
DisplayMetricsHolder.initDisplayMetrics(getContext());
|
|
// Check changes to both window and screen display metrics since they may not update at the same time.
|
|
if (!areMetricsEqual(mWindowMetrics, DisplayMetricsHolder.getWindowDisplayMetrics()) ||
|
|
!areMetricsEqual(mScreenMetrics, DisplayMetricsHolder.getScreenDisplayMetrics())) {
|
|
mWindowMetrics.setTo(DisplayMetricsHolder.getWindowDisplayMetrics());
|
|
mScreenMetrics.setTo(DisplayMetricsHolder.getScreenDisplayMetrics());
|
|
emitUpdateDimensionsEvent();
|
|
}
|
|
}
|
|
|
|
private boolean areMetricsEqual(DisplayMetrics displayMetrics, DisplayMetrics otherMetrics) {
|
|
if (Build.VERSION.SDK_INT >= 17) {
|
|
return displayMetrics.equals(otherMetrics);
|
|
} else {
|
|
// DisplayMetrics didn't have an equals method before API 17.
|
|
// Check all public fields manually.
|
|
return displayMetrics.widthPixels == otherMetrics.widthPixels &&
|
|
displayMetrics.heightPixels == otherMetrics.heightPixels &&
|
|
displayMetrics.density == otherMetrics.density &&
|
|
displayMetrics.densityDpi == otherMetrics.densityDpi &&
|
|
displayMetrics.scaledDensity == otherMetrics.scaledDensity &&
|
|
displayMetrics.xdpi == otherMetrics.xdpi &&
|
|
displayMetrics.ydpi == otherMetrics.ydpi;
|
|
}
|
|
}
|
|
|
|
private void emitOrientationChanged(final int newRotation) {
|
|
String name;
|
|
double rotationDegrees;
|
|
boolean isLandscape = false;
|
|
|
|
switch (newRotation) {
|
|
case Surface.ROTATION_0:
|
|
name = "portrait-primary";
|
|
rotationDegrees = 0.0;
|
|
break;
|
|
case Surface.ROTATION_90:
|
|
name = "landscape-primary";
|
|
rotationDegrees = -90.0;
|
|
isLandscape = true;
|
|
break;
|
|
case Surface.ROTATION_180:
|
|
name = "portrait-secondary";
|
|
rotationDegrees = 180.0;
|
|
break;
|
|
case Surface.ROTATION_270:
|
|
name = "landscape-secondary";
|
|
rotationDegrees = 90.0;
|
|
isLandscape = true;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
WritableMap map = Arguments.createMap();
|
|
map.putString("name", name);
|
|
map.putDouble("rotationDegrees", rotationDegrees);
|
|
map.putBoolean("isLandscape", isLandscape);
|
|
|
|
sendEvent("namedOrientationDidChange", map);
|
|
}
|
|
|
|
private void emitUpdateDimensionsEvent() {
|
|
mReactInstanceManager
|
|
.getCurrentReactContext()
|
|
.getNativeModule(DeviceInfoModule.class)
|
|
.emitUpdateDimensionsEvent();
|
|
}
|
|
|
|
private void sendEvent(String eventName, @Nullable WritableMap params) {
|
|
if (mReactInstanceManager != null) {
|
|
mReactInstanceManager.getCurrentReactContext()
|
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
.emit(eventName, params);
|
|
}
|
|
}
|
|
}
|
|
}
|