mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-03 09:35:11 +08:00
Summary:
The PR description has been updated to reflect the new approach.
**Breaking Change Summary**
On Android, the following properties now return a different number:
- `Dimensions.get('window').fontScale`
- `Dimensions.get('screen').fontScale`
- `PixelRatio.getFontScale()`
This is a breaking change to anyone who was using these properties because the meaning of these properties has now changed.
These properties used to return a value representing font scale times density ([`DisplayMetrics.scaledDensity`](https://developer.android.com/reference/android/util/DisplayMetrics.html#scaledDensity)). Now they return a value representing just font scale ([`Configuration.fontScale`](https://developer.android.com/reference/android/content/res/Configuration.html#fontScale)).
**PR Description**
This PR changes a few things:
- Correctly exposes the font scale to JavaScript as `Dimensions.get('screen').fontScale`. UIManager was exporting `DisplayMetrics.scaledDensity` under the name `fontScale`. How
Closes https://github.com/facebook/react-native/pull/11008
Differential Revision: D4558207
Pulled By: astreet
fbshipit-source-id: 096ce7b28051325dfd45fdb2a14b5e9b7d3bc46f
412 lines
15 KiB
Java
412 lines
15 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.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() {
|
|
mReactInstanceManager
|
|
.getCurrentReactContext()
|
|
.getNativeModule(UIManagerModule.class)
|
|
.emitUpdateDimensionsEvent();
|
|
}
|
|
|
|
private void sendEvent(String eventName, @Nullable WritableMap params) {
|
|
if (mReactInstanceManager != null) {
|
|
mReactInstanceManager.getCurrentReactContext()
|
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
.emit(eventName, params);
|
|
}
|
|
}
|
|
}
|
|
}
|