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
This commit is contained in:
Andrei Coman 2016-09-06 03:54:27 -07:00 committed by Facebook Github Bot 4
parent 5d240a8ed3
commit f07ca31303
5 changed files with 164 additions and 25 deletions

View File

@ -23,7 +23,9 @@
android:theme="@style/Theme.ReactNative.AppCompat.Light" >
<activity
android:name=".UIExplorerActivity"
android:label="@string/app_name" >
android:label="@string/app_name"
android:screenOrientation="fullSensor"
android:configChanges="orientation|screenSize" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2013-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.
*
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @providesModule OrientationChangeExample
* @flow
*/
'use strict';
const React = require('react');
const ReactNative = require('react-native');
const {
DeviceEventEmitter,
Text,
View,
} = ReactNative;
import type EmitterSubscription from 'EmitterSubscription';
class OrientationChangeExample extends React.Component {
_orientationSubscription: EmitterSubscription;
state = {
currentOrientation: '',
orientationDegrees: 0,
isLandscape: false,
};
componentDidMount() {
this._orientationSubscription = DeviceEventEmitter.addListener(
'namedOrientationDidChange', this._onOrientationChange,
);
}
componentWillUnmount() {
this._orientationSubscription.remove();
}
_onOrientationChange = (orientation: Object) => {
this.setState({
currentOrientation: orientation.name,
orientationDegrees: orientation.rotationDegrees,
isLandscape: orientation.isLandscape,
});
}
render() {
return (
<View>
<Text>{JSON.stringify(this.state)}</Text>
</View>
);
}
}
exports.title = 'OrientationChangeExample';
exports.description = 'listening to orientation changes';
exports.examples = [
{
title: 'OrientationChangeExample',
description: 'listening to device orientation changes',
render() { return <OrientationChangeExample />; },
},
];

View File

@ -22,14 +22,12 @@
*/
'use strict';
const React = require('React');
export type UIExplorerExample = {
key: string;
module: React.Component;
key: string,
module: Object,
};
var ComponentExamples: Array<UIExplorerExample> = [
const ComponentExamples: Array<UIExplorerExample> = [
{
key: 'ActivityIndicatorExample',
module: require('./ActivityIndicatorExample'),
@ -108,7 +106,7 @@ var ComponentExamples: Array<UIExplorerExample> = [
},
];
const APIExamples = [
const APIExamples: Array<UIExplorerExample> = [
{
key: 'AccessibilityAndroidExample',
module: require('./AccessibilityAndroidExample'),
@ -177,6 +175,10 @@ const APIExamples = [
key: 'NetInfoExample',
module: require('./NetInfoExample'),
},
{
key: 'OrientationChangeExample',
module: require('./OrientationChangeExample'),
},
{
key: 'PanResponderExample',
module: require('./PanResponderExample'),

View File

@ -23,8 +23,8 @@
'use strict';
export type UIExplorerExample = {
key: string;
module: Object;
key: string,
module: Object,
};
const ComponentExamples: Array<UIExplorerExample> = [
@ -235,6 +235,10 @@ const APIExamples: Array<UIExplorerExample> = [
key: 'NetInfoExample',
module: require('./NetInfoExample'),
},
{
key: 'OrientationChangeExample',
module: require('./OrientationChangeExample'),
},
{
key: 'PanResponderExample',
module: require('./PanResponderExample'),

View File

@ -11,14 +11,17 @@ package com.facebook.react;
import javax.annotation.Nullable;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.AttributeSet;
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;
@ -30,10 +33,10 @@ 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.JSTouchDispatcher;
import com.facebook.react.uimanager.UIManagerModule;
import com.facebook.react.uimanager.events.EventDispatcher;
@ -56,7 +59,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
private @Nullable ReactInstanceManager mReactInstanceManager;
private @Nullable String mJSModuleName;
private @Nullable Bundle mLaunchOptions;
private @Nullable KeyboardListener mKeyboardListener;
private @Nullable CustomGlobalLayoutListener mCustomGlobalLayoutListener;
private @Nullable OnGenericMotionListener mOnGenericMotionListener;
private int mRootViewTag;
private boolean mWasMeasured = false;
@ -171,7 +174,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mIsAttachedToInstance) {
getViewTreeObserver().addOnGlobalLayoutListener(getKeyboardListener());
getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener());
}
}
@ -179,7 +182,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mIsAttachedToInstance) {
getViewTreeObserver().removeOnGlobalLayoutListener(getKeyboardListener());
getViewTreeObserver().removeOnGlobalLayoutListener(getCustomGlobalLayoutListener());
}
}
@ -255,11 +258,11 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
mWasMeasured = true;
}
private KeyboardListener getKeyboardListener() {
if (mKeyboardListener == null) {
mKeyboardListener = new KeyboardListener();
private CustomGlobalLayoutListener getCustomGlobalLayoutListener() {
if (mCustomGlobalLayoutListener == null) {
mCustomGlobalLayoutListener = new CustomGlobalLayoutListener();
}
return mKeyboardListener;
return mCustomGlobalLayoutListener;
}
private void attachToReactInstanceManager() {
@ -269,7 +272,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
mIsAttachedToInstance = true;
Assertions.assertNotNull(mReactInstanceManager).attachMeasuredRootView(this);
getViewTreeObserver().addOnGlobalLayoutListener(getKeyboardListener());
getViewTreeObserver().addOnGlobalLayoutListener(getCustomGlobalLayoutListener());
}
@Override
@ -291,13 +294,14 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
mRootViewTag = rootViewTag;
}
private class KeyboardListener implements ViewTreeObserver.OnGlobalLayoutListener {
private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
private final Rect mVisibleViewArea;
private final int mMinKeyboardHeightDetected;
private int mKeyboardHeight = 0;
private int mDeviceRotation = 0;
/* package */ KeyboardListener() {
/* package */ CustomGlobalLayoutListener() {
mVisibleViewArea = new Rect();
mMinKeyboardHeightDetected = (int) PixelUtil.toPixelFromDIP(60);
}
@ -305,16 +309,17 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
@Override
public void onGlobalLayout() {
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
mReactInstanceManager.getCurrentReactContext() == null) {
FLog.w(
ReactConstants.TAG,
"Unable to dispatch keyboard events in JS as the react instance has not been attached");
mReactInstanceManager.getCurrentReactContext() == null) {
return;
}
checkForKeyboardEvents();
checkForDeviceOrientationChanges();
}
private void checkForKeyboardEvents() {
getRootView().getWindowVisibleDisplayFrame(mVisibleViewArea);
final int heightDiff =
DisplayMetricsHolder.getWindowDisplayMetrics().heightPixels - mVisibleViewArea.bottom;
DisplayMetricsHolder.getWindowDisplayMetrics().heightPixels - mVisibleViewArea.bottom;
if (mKeyboardHeight != heightDiff && heightDiff > mMinKeyboardHeightDetected) {
// keyboard is now showing, or the keyboard height has changed
mKeyboardHeight = heightDiff;
@ -333,6 +338,52 @@ public class ReactRootView extends SizeMonitoringFrameLayout implements RootView
}
}
private void checkForDeviceOrientationChanges() {
final int rotation =
((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay().getRotation();
if (mDeviceRotation == rotation) {
return;
}
mDeviceRotation = rotation;
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 sendEvent(String eventName, @Nullable WritableMap params) {
if (mReactInstanceManager != null) {
mReactInstanceManager.getCurrentReactContext()