Skip to content

Commit f07ca31

Browse files
andreicoman11Facebook Github Bot 4
authored andcommitted
Listen to device orientation changes
Summary: Similar to iOS, send device orientation changes events. This does not have the `getCurrentOrientation` method, because it's not used. If necessary, we'll add it separately. This also adds a simple example for testing. We listen to orientation changes in `onGlobalLayout`, and check if the rotation of the device has changed. If it has, we emit the event. But: - `onGlobalLayout` (and `onConfigurationChanged` - which is the method usually used for checking for device orientation changes) is *not* called when the device goes from landscape to reverse landscape (same with portrait), as that is not a relayout / configuration change. We could detect if this happens with the help of an `OrientationEventListener`. However, this listener notifies you if the degree of the phone changes by a single degree, which means that you need to know by how many degrees the phone needs to change in order for the orientation to change. I haven't looked into how accurate this could be, but I suspect that in practice it would cause a lot of bugs. A simple `abgs` and google search reveals that everybody uses a different margin for detecting a rotation change (from 30 to 45 degrees), so I suspect that this won't work as expected in practice. Therefore, we're not using this here, and we're sticking to what android provides via `onConfigurationChanged`. If we find that we have issues because users need to know when the user goes from landscape to reverse landscape, then we'll have to revisit this. Reviewed By: foghina Differential Revision: D3797521 fbshipit-source-id: 62508efd342a9a4b41b42b6138c73553cfdefebc
1 parent 5d240a8 commit f07ca31

File tree

5 files changed

+164
-25
lines changed

5 files changed

+164
-25
lines changed

Examples/UIExplorer/android/app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
android:theme="@style/Theme.ReactNative.AppCompat.Light" >
2424
<activity
2525
android:name=".UIExplorerActivity"
26-
android:label="@string/app_name" >
26+
android:label="@string/app_name"
27+
android:screenOrientation="fullSensor"
28+
android:configChanges="orientation|screenSize" >
2729
<intent-filter>
2830
<action android:name="android.intent.action.MAIN" />
2931
<category android:name="android.intent.category.LAUNCHER" />
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Copyright (c) 2013-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* The examples provided by Facebook are for non-commercial testing and
10+
* evaluation purposes only.
11+
*
12+
* Facebook reserves all rights not expressly granted.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15+
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
17+
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
18+
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*
21+
* @providesModule OrientationChangeExample
22+
* @flow
23+
*/
24+
'use strict';
25+
26+
const React = require('react');
27+
const ReactNative = require('react-native');
28+
const {
29+
DeviceEventEmitter,
30+
Text,
31+
View,
32+
} = ReactNative;
33+
34+
import type EmitterSubscription from 'EmitterSubscription';
35+
36+
class OrientationChangeExample extends React.Component {
37+
_orientationSubscription: EmitterSubscription;
38+
39+
state = {
40+
currentOrientation: '',
41+
orientationDegrees: 0,
42+
isLandscape: false,
43+
};
44+
45+
componentDidMount() {
46+
this._orientationSubscription = DeviceEventEmitter.addListener(
47+
'namedOrientationDidChange', this._onOrientationChange,
48+
);
49+
}
50+
51+
componentWillUnmount() {
52+
this._orientationSubscription.remove();
53+
}
54+
55+
_onOrientationChange = (orientation: Object) => {
56+
this.setState({
57+
currentOrientation: orientation.name,
58+
orientationDegrees: orientation.rotationDegrees,
59+
isLandscape: orientation.isLandscape,
60+
});
61+
}
62+
63+
render() {
64+
return (
65+
<View>
66+
<Text>{JSON.stringify(this.state)}</Text>
67+
</View>
68+
);
69+
}
70+
}
71+
72+
exports.title = 'OrientationChangeExample';
73+
exports.description = 'listening to orientation changes';
74+
exports.examples = [
75+
{
76+
title: 'OrientationChangeExample',
77+
description: 'listening to device orientation changes',
78+
render() { return <OrientationChangeExample />; },
79+
},
80+
];

Examples/UIExplorer/js/UIExplorerList.android.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@
2222
*/
2323
'use strict';
2424

25-
const React = require('React');
26-
2725
export type UIExplorerExample = {
28-
key: string;
29-
module: React.Component;
26+
key: string,
27+
module: Object,
3028
};
3129

32-
var ComponentExamples: Array<UIExplorerExample> = [
30+
const ComponentExamples: Array<UIExplorerExample> = [
3331
{
3432
key: 'ActivityIndicatorExample',
3533
module: require('./ActivityIndicatorExample'),
@@ -108,7 +106,7 @@ var ComponentExamples: Array<UIExplorerExample> = [
108106
},
109107
];
110108

111-
const APIExamples = [
109+
const APIExamples: Array<UIExplorerExample> = [
112110
{
113111
key: 'AccessibilityAndroidExample',
114112
module: require('./AccessibilityAndroidExample'),
@@ -177,6 +175,10 @@ const APIExamples = [
177175
key: 'NetInfoExample',
178176
module: require('./NetInfoExample'),
179177
},
178+
{
179+
key: 'OrientationChangeExample',
180+
module: require('./OrientationChangeExample'),
181+
},
180182
{
181183
key: 'PanResponderExample',
182184
module: require('./PanResponderExample'),

Examples/UIExplorer/js/UIExplorerList.ios.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
'use strict';
2424

2525
export type UIExplorerExample = {
26-
key: string;
27-
module: Object;
26+
key: string,
27+
module: Object,
2828
};
2929

3030
const ComponentExamples: Array<UIExplorerExample> = [
@@ -235,6 +235,10 @@ const APIExamples: Array<UIExplorerExample> = [
235235
key: 'NetInfoExample',
236236
module: require('./NetInfoExample'),
237237
},
238+
{
239+
key: 'OrientationChangeExample',
240+
module: require('./OrientationChangeExample'),
241+
},
238242
{
239243
key: 'PanResponderExample',
240244
module: require('./PanResponderExample'),

ReactAndroid/src/main/java/com/facebook/react/ReactRootView.java

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111

1212
import javax.annotation.Nullable;
1313

14+
import android.app.Activity;
1415
import android.content.Context;
1516
import android.graphics.Rect;
1617
import android.os.Bundle;
1718
import android.util.AttributeSet;
1819
import android.view.MotionEvent;
20+
import android.view.Surface;
1921
import android.view.View;
2022
import android.view.ViewGroup;
2123
import android.view.ViewTreeObserver;
24+
import android.view.WindowManager;
2225

2326
import com.facebook.common.logging.FLog;
2427
import com.facebook.infer.annotation.Assertions;
@@ -30,10 +33,10 @@
3033
import com.facebook.react.common.annotations.VisibleForTesting;
3134
import com.facebook.react.modules.core.DeviceEventManagerModule;
3235
import com.facebook.react.uimanager.DisplayMetricsHolder;
36+
import com.facebook.react.uimanager.JSTouchDispatcher;
3337
import com.facebook.react.uimanager.PixelUtil;
3438
import com.facebook.react.uimanager.RootView;
3539
import com.facebook.react.uimanager.SizeMonitoringFrameLayout;
36-
import com.facebook.react.uimanager.JSTouchDispatcher;
3740
import com.facebook.react.uimanager.UIManagerModule;
3841
import com.facebook.react.uimanager.events.EventDispatcher;
3942

@@ -56,7 +59,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
5659
private @Nullable ReactInstanceManager mReactInstanceManager;
5760
private @Nullable String mJSModuleName;
5861
private @Nullable Bundle mLaunchOptions;
59-
private @Nullable KeyboardListener mKeyboardListener;
62+
private @Nullable CustomGlobalLayoutListener mCustomGlobalLayoutListener;
6063
private @Nullable OnGenericMotionListener mOnGenericMotionListener;
6164
private int mRootViewTag;
6265
private boolean mWasMeasured = false;
@@ -171,15 +174,15 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto
171174
protected void onAttachedToWindow() {
172175
super.onAttachedToWindow();
173176
if (mIsAttachedToInstance) {
174-
getViewTreeObserver().addOnGlobalLayoutListener(getKeyboardListener());
177+
getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener());
175178
}
176179
}
177180

178181
@Override
179182
protected void onDetachedFromWindow() {
180183
super.onDetachedFromWindow();
181184
if (mIsAttachedToInstance) {
182-
getViewTreeObserver().removeOnGlobalLayoutListener(getKeyboardListener());
185+
getViewTreeObserver().removeOnGlobalLayoutListener(getCustomGlobalLayoutListener());
183186
}
184187
}
185188

@@ -255,11 +258,11 @@ public void unmountReactApplication() {
255258
mWasMeasured = true;
256259
}
257260

258-
private KeyboardListener getKeyboardListener() {
259-
if (mKeyboardListener == null) {
260-
mKeyboardListener = new KeyboardListener();
261+
private CustomGlobalLayoutListener getCustomGlobalLayoutListener() {
262+
if (mCustomGlobalLayoutListener == null) {
263+
mCustomGlobalLayoutListener = new CustomGlobalLayoutListener();
261264
}
262-
return mKeyboardListener;
265+
return mCustomGlobalLayoutListener;
263266
}
264267

265268
private void attachToReactInstanceManager() {
@@ -269,7 +272,7 @@ private void attachToReactInstanceManager() {
269272

270273
mIsAttachedToInstance = true;
271274
Assertions.assertNotNull(mReactInstanceManager).attachMeasuredRootView(this);
272-
getViewTreeObserver().addOnGlobalLayoutListener(getKeyboardListener());
275+
getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener());
273276
}
274277

275278
@Override
@@ -291,30 +294,32 @@ public void setRootViewTag(int rootViewTag) {
291294
mRootViewTag = rootViewTag;
292295
}
293296

294-
private class KeyboardListener implements ViewTreeObserver.OnGlobalLayoutListener {
297+
private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
295298
private final Rect mVisibleViewArea;
296299
private final int mMinKeyboardHeightDetected;
297300

298301
private int mKeyboardHeight = 0;
302+
private int mDeviceRotation = 0;
299303

300-
/* package */ KeyboardListener() {
304+
/* package */ CustomGlobalLayoutListener() {
301305
mVisibleViewArea = new Rect();
302306
mMinKeyboardHeightDetected = (int) PixelUtil.toPixelFromDIP(60);
303307
}
304308

305309
@Override
306310
public void onGlobalLayout() {
307311
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
308-
mReactInstanceManager.getCurrentReactContext() == null) {
309-
FLog.w(
310-
ReactConstants.TAG,
311-
"Unable to dispatch keyboard events in JS as the react instance has not been attached");
312+
mReactInstanceManager.getCurrentReactContext() == null) {
312313
return;
313314
}
315+
checkForKeyboardEvents();
316+
checkForDeviceOrientationChanges();
317+
}
314318

319+
private void checkForKeyboardEvents() {
315320
getRootView().getWindowVisibleDisplayFrame(mVisibleViewArea);
316321
final int heightDiff =
317-
DisplayMetricsHolder.getWindowDisplayMetrics().heightPixels - mVisibleViewArea.bottom;
322+
DisplayMetricsHolder.getWindowDisplayMetrics().heightPixels - mVisibleViewArea.bottom;
318323
if (mKeyboardHeight != heightDiff && heightDiff > mMinKeyboardHeightDetected) {
319324
// keyboard is now showing, or the keyboard height has changed
320325
mKeyboardHeight = heightDiff;
@@ -333,6 +338,52 @@ public void onGlobalLayout() {
333338
}
334339
}
335340

341+
private void checkForDeviceOrientationChanges() {
342+
final int rotation =
343+
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
344+
.getDefaultDisplay().getRotation();
345+
if (mDeviceRotation == rotation) {
346+
return;
347+
}
348+
mDeviceRotation = rotation;
349+
emitOrientationChanged(rotation);
350+
}
351+
352+
private void emitOrientationChanged(final int newRotation) {
353+
String name;
354+
double rotationDegrees;
355+
boolean isLandscape = false;
356+
357+
switch (newRotation) {
358+
case Surface.ROTATION_0:
359+
name = "portrait-primary";
360+
rotationDegrees = 0.0;
361+
break;
362+
case Surface.ROTATION_90:
363+
name = "landscape-primary";
364+
rotationDegrees = -90.0;
365+
isLandscape = true;
366+
break;
367+
case Surface.ROTATION_180:
368+
name = "portrait-secondary";
369+
rotationDegrees = 180.0;
370+
break;
371+
case Surface.ROTATION_270:
372+
name = "landscape-secondary";
373+
rotationDegrees = 90.0;
374+
isLandscape = true;
375+
break;
376+
default:
377+
return;
378+
}
379+
WritableMap map = Arguments.createMap();
380+
map.putString("name", name);
381+
map.putDouble("rotationDegrees", rotationDegrees);
382+
map.putBoolean("isLandscape", isLandscape);
383+
384+
sendEvent("namedOrientationDidChange", map);
385+
}
386+
336387
private void sendEvent(String eventName, @Nullable WritableMap params) {
337388
if (mReactInstanceManager != null) {
338389
mReactInstanceManager.getCurrentReactContext()

0 commit comments

Comments
 (0)