From c9aeaf675dcf0292d148950d0fb020eeb13d3af1 Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Wed, 2 Aug 2017 21:04:06 -0700 Subject: [PATCH] Separate window dimensions change from orientation 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. Closes https://github.com/facebook/react-native/pull/15181 Differential Revision: D5552195 Pulled By: shergin fbshipit-source-id: d1f190cb960090468886ff56cda58cac296745db --- .../com/facebook/react/ReactRootView.java | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java index 89a4fe55da7c28..fb7601750beada 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java +++ b/ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java @@ -16,6 +16,7 @@ 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; @@ -357,6 +358,8 @@ private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLay private int mKeyboardHeight = 0; private int mDeviceRotation = 0; + private DisplayMetrics mWindowMetrics = new DisplayMetrics(); + private DisplayMetrics mScreenMetrics = new DisplayMetrics(); /* package */ CustomGlobalLayoutListener() { DisplayMetricsHolder.initDisplayMetricsIfNotInitialized(getContext().getApplicationContext()); @@ -372,6 +375,7 @@ public void onGlobalLayout() { } checkForKeyboardEvents(); checkForDeviceOrientationChanges(); + checkForDeviceDimensionsChanges(); } private void checkForKeyboardEvents() { @@ -404,13 +408,37 @@ private void checkForDeviceOrientationChanges() { 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 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;