/** * 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.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.ReactContext; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; import com.facebook.react.common.annotations.VisibleForTesting; import com.facebook.react.modules.core.DeviceEventManagerModule; 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; /** * 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 it's 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 mLaunchOptions; private @Nullable CustomGlobalLayoutListener mCustomGlobalLayoutListener; private @Nullable ReactRootViewEventListener mRootViewEventListener; private int mRootViewTag; private boolean mWasMeasured = false; private boolean mIsAttachedToInstance = false; 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) { setMeasuredDimension( MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); mWasMeasured = true; // Check if we were waiting for onMeasure to attach the root view if (mReactInstanceManager != null && !mIsAttachedToInstance) { // Enqueue it to UIThread not to block onMeasure waiting for the catalyst instance creation UiThreadUtil.runOnUiThread(new Runnable() { @Override public void run() { attachToReactInstanceManager(); } }); } } @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) { getViewTreeObserver().removeOnGlobalLayoutListener(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 launchOptions) { 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; mLaunchOptions = launchOptions; if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { mReactInstanceManager.createReactContextInBackground(); } // We need to wait for the initial onMeasure, if this view has not yet been measured, we set which // will make this view startReactApplication itself to instance manager once onMeasure is called. if (mWasMeasured) { attachToReactInstanceManager(); } } /** * 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); } /* package */ @Nullable Bundle getLaunchOptions() { return mLaunchOptions; } /** * Is used by unit test to setup mWasMeasured and mIsAttachedToWindow flags, that will let this * view to be properly attached to catalyst instance by startReactApplication call */ @VisibleForTesting /* package */ void simulateAttachForTesting() { mIsAttachedToInstance = true; mWasMeasured = true; } private CustomGlobalLayoutListener getCustomGlobalLayoutListener() { if (mCustomGlobalLayoutListener == null) { mCustomGlobalLayoutListener = new CustomGlobalLayoutListener(); } return mCustomGlobalLayoutListener; } private void attachToReactInstanceManager() { if (mIsAttachedToInstance) { return; } mIsAttachedToInstance = true; Assertions.assertNotNull(mReactInstanceManager).attachMeasuredRootView(this); getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener()); } @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; /* package */ CustomGlobalLayoutListener() { mVisibleViewArea = new Rect(); mMinKeyboardHeightDetected = (int) PixelUtil.toPixelFromDIP(60); } @Override public void onGlobalLayout() { if (mReactInstanceManager == null || !mIsAttachedToInstance || mReactInstanceManager.getCurrentReactContext() == null) { return; } checkForKeyboardEvents(); checkForDeviceOrientationChanges(); } 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; // It's important to repopulate DisplayMetrics and export them before emitting the // orientation change event, so that the Dimensions object returns the correct new values. DisplayMetricsHolder.initDisplayMetrics(getContext()); emitUpdateDimensionsEvent(); emitOrientationChanged(rotation); } 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() { DisplayMetrics windowDisplayMetrics = DisplayMetricsHolder.getWindowDisplayMetrics(); DisplayMetrics screenDisplayMetrics = DisplayMetricsHolder.getScreenDisplayMetrics(); WritableMap windowDisplayMetricsMap = Arguments.createMap(); windowDisplayMetricsMap.putInt("width", windowDisplayMetrics.widthPixels); windowDisplayMetricsMap.putInt("height", windowDisplayMetrics.heightPixels); windowDisplayMetricsMap.putDouble("scale", windowDisplayMetrics.density); windowDisplayMetricsMap.putDouble("fontScale", windowDisplayMetrics.scaledDensity); windowDisplayMetricsMap.putDouble("densityDpi", windowDisplayMetrics.densityDpi); WritableMap screenDisplayMetricsMap = Arguments.createMap(); screenDisplayMetricsMap.putInt("width", screenDisplayMetrics.widthPixels); screenDisplayMetricsMap.putInt("height", screenDisplayMetrics.heightPixels); screenDisplayMetricsMap.putDouble("scale", screenDisplayMetrics.density); screenDisplayMetricsMap.putDouble("fontScale", screenDisplayMetrics.scaledDensity); screenDisplayMetricsMap.putDouble("densityDpi", screenDisplayMetrics.densityDpi); WritableMap dimensionsMap = Arguments.createMap(); dimensionsMap.putMap("windowPhysicalPixels", windowDisplayMetricsMap); dimensionsMap.putMap("screenPhysicalPixels", screenDisplayMetricsMap); sendEvent("didUpdateDimensions", dimensionsMap); } private void sendEvent(String eventName, @Nullable WritableMap params) { if (mReactInstanceManager != null) { mReactInstanceManager.getCurrentReactContext() .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(eventName, params); } } } }