mirror of
https://github.com/zhigang1992/react-native.git
synced 2026-04-01 13:03:08 +08:00
Add support for Android TV devices
Summary: <!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. You can learn more about contributing to React Native here: http://facebook.github.io/react-native/docs/contributing.html Happy contributing! --> * To be on par with Apple TV support, this makes it possible to run React Native apps on Android TV devices (See also: https://react-native.canny.io/feature-requests/p/android-tv-support) * These changes also make it possible to navigate through the app using D-PAD buttons that are present on some mobile devices * Since these changes affect, among others, `ReactRootView.java` and `Touchable.js` code and are closely related to Apple TV implementation, it makes sense for them to be included in the core - React native apps can be launched on Android TV devices and properly render their content - Navigation is possible using left, right, top, bottom arrows from the remote (or D-PAD) - Touchable components can handle D-PAD center button press events and correctly fire their `onPress` handlers - Touchable components will receive `onPressIn` and `onPressOut` events and can react to focus/blur changes appropriately (just like on Apple TV) - `Platform` constants allow to check if the react-native app is running on TV (`Platform.isTV`) - `ScrollView`s behave correctly (same as native implementation) when switching to view outside bounds – that is, the container would scroll such that the newly focused element is fully visible - Native "clicking" sounds are played when moving between focusable elements - Play/Pause click event is send to `TVEventHandler` - Rewind and FastForward events are send to `TVEventHandler` - Back button behaves as a normal Android back button - Diagonal buttons work correctly on Android TV, e.g. if there is no button directly to the right from the focused one, but there is one to the right but a bit higher/lower it will grab focus - Dev menu can be accessed by long pressing fast forward button A demo showing RNTester app running on Android TV device (Amazon Fire TV Stick) can be found here: [](http://www.youtube.com/watch?v=EzIQErHhY20) - `TextInput` will not work on Android TV devices. There's an issue with native `ReactEditText` implementation that prevents it from receiving focus. This makes it impossible to navigate to `TextInput`. This will be fixed next, but will be included in a separate Pull Request - ~Overlay permissions cannot be granted on Android TV devices running Android version >= 6.0 This is because the overlay permission can only be granted by firing an Intent to open settings page (`ACTION_MANAGE_OVERLAY_PERMISSION`). Since this page does not exist on TV devices the permission cannot be requested. This will make the app crash when trying to open dev menu (⌘+M) or displaying a redbox error. Note: This does not affect devices running Android version < 6.0 (for example Amazon Fire TV Stick)~ This is now fixed by: https://github.com/facebook/react-native/pull/16596 * Launch the RNTester app on Android TV device. * Ensure it launches without a crash * Ensure basic navigation is possible * Ensure Touchable components can receive select events * Ensure the changes do not break current Android and iOS mobile devices functionality. * Ensure the changes do not break current Apple TV functionality. [RNAndroidTVDemo video](http://img.youtube.com/vi/EzIQErHhY20/0.jpg) * Added `ReactAndroidTVViewManager` that handles TV `KeyEvent`s and dispatches events to JS - This is the core that enables basic navigation functionality on Android TV devices * Following the above change we copy `TVEventHandler.ios.js` into `TVEventHandler.android.js` to enable JS to pick up those native navigation events and dispatch them further to subscribed views. (Note: We do not have a native `TVNavigationEventEmitter` implementation on Android, thus this file is slightly modified, e.g. it does pass `null` to `NativeEventEmitter` constructor) * Added `uiMode` to `AndroidInfoModule`. (**Note**: This required changing `extends BaseJavaModule` to `extends ReactContextBaseJavaModule` to be able to use `getSystemService` which requires `Context` instance! * Added `isTV` constants to both `Platform.ios.js` (keeping the deprecated `isTVOS` as well) and `Platform.android.js` * Changed condition check on `Touchable.js` to use the newly added `isTV` flag to properly handle TV navigation events on Android as well * Added `LEANBACK_LAUNCHER` to `RNTester` `intent-filter` so that it is possible to launch it on Android TV devices. * See also a PR to `react-native-website` repo with updated docs for Android TV: https://github.com/facebook/react-native-website/pull/59 - [ ] Fix `TextInput` components handling by allowing them to be focused and making a proper navigation between them (and/or other components) possible. One thing to note here that the default behavior to immediately open software keyboard when focused on `TextInput` field will need to be adjusted on Android TV as well) - [x] Fix overlay permissions issue by changing the way redbox/dev menu are displayed (see: https://github.com/facebook/react-native/pull/16596) - [ ] Adjust placement of TV-related files (e.g. the `TVEventHandler.js` file is placed inside `AppleTV` directory which is not accurate, since it does handle Android TV events as well) Previous discussion: https://github.com/SoftwareMansion/react-native/pull/1 <!-- Help reviewers and the release process by writing your own release notes **INTERNAL and MINOR tagged notes will not be included in the next version's final release notes.** CATEGORY [----------] TYPE [ CLI ] [-------------] LOCATION [ DOCS ] [ BREAKING ] [-------------] [ GENERAl ] [ BUGFIX ] [-{Component}-] [ INTERNAL ] [ ENHANCEMENT ] [ {File} ] [ IOS ] [ FEATURE ] [ {Directory} ] |-----------| [ ANDROID ] [ MINOR ] [ {Framework} ] - | {Message} | [----------] [-------------] [-------------] |-----------| [CATEGORY] [TYPE] [LOCATION] - MESSAGE EXAMPLES: [IOS] [BREAKING] [FlatList] - Change a thing that breaks other things [ANDROID] [BUGFIX] [TextInput] - Did a thing to TextInput [CLI] [FEATURE] [local-cli/info/info.js] - CLI easier to do things with [DOCS] [BUGFIX] [GettingStarted.md] - Accidentally a thing/word [GENERAL] [ENHANCEMENT] [Yoga] - Added new yoga thing/position [INTERNAL] [FEATURE] [./scripts] - Added thing to script that nobody will see --> [ANDROID] [FEATURE] [TV] - Added support for Android TV devices Closes https://github.com/facebook/react-native/pull/16500 Differential Revision: D6536847 Pulled By: hramos fbshipit-source-id: 17bbb11e8583b97f195ced5fd9762f8902fb8a3d
This commit is contained in:
committed by
Facebook Github Bot
parent
d2f05740a8
commit
b7bb2e5745
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @providesModule TVEventHandler
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
function TVEventHandler() {}
|
||||
|
||||
TVEventHandler.prototype.enable = function(component: ?any, callback: Function) {};
|
||||
|
||||
TVEventHandler.prototype.disable = function() {};
|
||||
|
||||
module.exports = TVEventHandler;
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const React = require('React');
|
||||
const Platform = require('Platform');
|
||||
const TVNavigationEventEmitter = require('NativeModules').TVNavigationEventEmitter;
|
||||
const NativeEventEmitter = require('NativeEventEmitter');
|
||||
|
||||
@@ -19,13 +19,13 @@ function TVEventHandler() {
|
||||
}
|
||||
|
||||
TVEventHandler.prototype.enable = function(component: ?any, callback: Function) {
|
||||
if (!TVNavigationEventEmitter) {
|
||||
if (Platform.OS === 'ios' && !TVNavigationEventEmitter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.__nativeTVNavigationEventEmitter = new NativeEventEmitter(TVNavigationEventEmitter);
|
||||
this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener(
|
||||
'onTVNavEvent',
|
||||
'onHWKeyEvent',
|
||||
(data) => {
|
||||
if (callback) {
|
||||
callback(component, data);
|
||||
@@ -15,17 +15,13 @@ const PropTypes = require('prop-types');
|
||||
*/
|
||||
const TVViewPropTypes = {
|
||||
/**
|
||||
* *(Apple TV only)* When set to true, this view will be focusable
|
||||
* and navigable using the Apple TV remote.
|
||||
*
|
||||
* @platform ios
|
||||
* When set to true, this view will be focusable
|
||||
* and navigable using the TV remote.
|
||||
*/
|
||||
isTVSelectable: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* *(Apple TV only)* May be set to true to force the Apple TV focus engine to move focus to this view.
|
||||
*
|
||||
* @platform ios
|
||||
* May be set to true to force the TV focus engine to move focus to this view.
|
||||
*/
|
||||
hasTVPreferredFocus: PropTypes.bool,
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class Button extends React.Component<{
|
||||
title: string,
|
||||
onPress: () => any,
|
||||
color?: ?string,
|
||||
hasTVPreferredFocus?: ?boolean,
|
||||
accessibilityLabel?: ?string,
|
||||
disabled?: ?boolean,
|
||||
testID?: ?string,
|
||||
@@ -75,6 +76,10 @@ class Button extends React.Component<{
|
||||
* If true, disable all interactions for this component.
|
||||
*/
|
||||
disabled: PropTypes.bool,
|
||||
/**
|
||||
* TV preferred focus (see documentation for the View component).
|
||||
*/
|
||||
hasTVPreferredFocus: PropTypes.bool,
|
||||
/**
|
||||
* Handler to be called when the user taps the button
|
||||
*/
|
||||
@@ -83,12 +88,6 @@ class Button extends React.Component<{
|
||||
* Used to locate this view in end-to-end tests.
|
||||
*/
|
||||
testID: PropTypes.string,
|
||||
/**
|
||||
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
hasTVPreferredFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -315,7 +315,7 @@ const LONG_PRESS_ALLOWED_MOVEMENT = 10;
|
||||
*/
|
||||
const TouchableMixin = {
|
||||
componentDidMount: function() {
|
||||
if (!Platform.isTVOS) {
|
||||
if (!Platform.isTV) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ const TouchableMixin = {
|
||||
} else if (evt.eventType === 'blur') {
|
||||
cmp.touchableHandleActivePressOut && cmp.touchableHandleActivePressOut(evt);
|
||||
} else if (evt.eventType === 'select') {
|
||||
cmp.touchableHandlePress && cmp.touchableHandlePress(evt);
|
||||
cmp.touchableHandlePress && !cmp.props.disabled && cmp.touchableHandlePress(evt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -82,6 +82,11 @@ const TouchableNativeFeedback = createReactClass({
|
||||
*/
|
||||
background: backgroundPropType,
|
||||
|
||||
/**
|
||||
* TV preferred focus (see documentation for the View component).
|
||||
*/
|
||||
hasTVPreferredFocus: PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Set to true to add the ripple effect to the foreground of the view, instead of the
|
||||
* background. This is useful if one of your child views has a background of its own, or you're
|
||||
@@ -156,7 +161,9 @@ const TouchableNativeFeedback = createReactClass({
|
||||
touchableHandleActivePressIn: function(e: Event) {
|
||||
this.props.onPressIn && this.props.onPressIn(e);
|
||||
this._dispatchPressedStateChange(true);
|
||||
this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY);
|
||||
if (this.pressInLocation) {
|
||||
this._dispatchHotspotUpdate(this.pressInLocation.locationX, this.pressInLocation.locationY);
|
||||
}
|
||||
},
|
||||
|
||||
touchableHandleActivePressOut: function(e: Event) {
|
||||
@@ -244,6 +251,8 @@ const TouchableNativeFeedback = createReactClass({
|
||||
testID: this.props.testID,
|
||||
onLayout: this.props.onLayout,
|
||||
hitSlop: this.props.hitSlop,
|
||||
isTVSelectable: true,
|
||||
hasTVPreferredFocus: this.props.hasTVPreferredFocus,
|
||||
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
|
||||
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
|
||||
onResponderGrant: this.touchableHandleResponderGrant,
|
||||
|
||||
@@ -128,9 +128,7 @@ const TouchableOpacity = createReactClass({
|
||||
*/
|
||||
activeOpacity: PropTypes.number,
|
||||
/**
|
||||
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
|
||||
*
|
||||
* @platform ios
|
||||
* TV preferred focus (see documentation for the View component).
|
||||
*/
|
||||
hasTVPreferredFocus: PropTypes.bool,
|
||||
/**
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @providesModule PlatformViewPropTypes
|
||||
* @flow
|
||||
*/
|
||||
|
||||
module.export = {};
|
||||
@@ -11,7 +11,10 @@
|
||||
const Platform = require('Platform');
|
||||
|
||||
let TVViewPropTypes = {};
|
||||
if (Platform.isTVOS) {
|
||||
// We need to always include TVViewPropTypes on Android
|
||||
// as unlike on iOS we can't detect TV devices at build time
|
||||
// and hence make view manager export a different list of native properties.
|
||||
if (Platform.isTV || Platform.OS === 'android') {
|
||||
TVViewPropTypes = require('TVViewPropTypes');
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ const Platform = {
|
||||
const constants = NativeModules.PlatformConstants;
|
||||
return constants && constants.isTesting;
|
||||
},
|
||||
get isTV(): boolean {
|
||||
const constants = NativeModules.PlatformConstants;
|
||||
return constants && constants.uiMode === 'tv';
|
||||
},
|
||||
select: (obj: Object) => 'android' in obj ? obj.android : obj.default,
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,13 @@ const Platform = {
|
||||
const constants = NativeModules.PlatformConstants;
|
||||
return constants ? constants.interfaceIdiom === 'pad' : false;
|
||||
},
|
||||
/**
|
||||
* Deprecated, use `isTV` instead.
|
||||
*/
|
||||
get isTVOS() {
|
||||
return Platform.isTV;
|
||||
},
|
||||
get isTV() {
|
||||
const constants = NativeModules.PlatformConstants;
|
||||
return constants ? constants.interfaceIdiom === 'tv' : false;
|
||||
},
|
||||
|
||||
@@ -20,9 +20,14 @@
|
||||
android:minSdkVersion="16"
|
||||
android:targetSdkVersion="23" />
|
||||
|
||||
<!--
|
||||
android:icon is used to display launcher icon on mobile devices.
|
||||
android:banner is used to display a rectangular banned launcher icon on Android TV devices.
|
||||
-->
|
||||
<application
|
||||
android:name=".RNTesterApplication"
|
||||
android:allowBackup="true"
|
||||
android:banner="@drawable/tv_banner"
|
||||
android:icon="@drawable/launcher_icon"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.ReactNative.AppCompat.Light" >
|
||||
@@ -34,6 +39,8 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- Needed to properly create a launch intent when running on Android TV -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
BIN
RNTester/android/app/src/main/res/drawable/tv_banner.png
Normal file
BIN
RNTester/android/app/src/main/res/drawable/tv_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -9,7 +9,7 @@
|
||||
|
||||
NSString *const RCTTVNavigationEventNotification = @"RCTTVNavigationEventNotification";
|
||||
|
||||
static NSString *const TVNavigationEventName = @"onTVNavEvent";
|
||||
static NSString *const TVNavigationEventName = @"onHWKeyEvent";
|
||||
|
||||
@implementation RCTTVNavigationEventEmitter
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ public class CatalystNativeJSToJavaParametersTestCase extends ReactIntegrationTe
|
||||
mRecordingTestModule = new RecordingTestModule();
|
||||
mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this)
|
||||
.addNativeModule(mRecordingTestModule)
|
||||
.addNativeModule(new AndroidInfoModule())
|
||||
.addNativeModule(new AndroidInfoModule(getContext()))
|
||||
.addNativeModule(new DeviceInfoModule(getContext()))
|
||||
.addNativeModule(new AppStateModule(getContext()))
|
||||
.addNativeModule(new FakeWebSocketModule())
|
||||
|
||||
@@ -88,7 +88,7 @@ public class CatalystUIManagerTestCase extends ReactIntegrationTestCase {
|
||||
|
||||
jsModule = ReactTestHelper.catalystInstanceBuilder(this)
|
||||
.addNativeModule(uiManager)
|
||||
.addNativeModule(new AndroidInfoModule())
|
||||
.addNativeModule(new AndroidInfoModule(getContext()))
|
||||
.addNativeModule(new DeviceInfoModule(getContext()))
|
||||
.addNativeModule(new AppStateModule(getContext()))
|
||||
.addNativeModule(new FakeWebSocketModule())
|
||||
|
||||
@@ -80,7 +80,7 @@ public class ProgressBarTestCase extends ReactIntegrationTestCase {
|
||||
|
||||
mInstance = ReactTestHelper.catalystInstanceBuilder(this)
|
||||
.addNativeModule(mUIManager)
|
||||
.addNativeModule(new AndroidInfoModule())
|
||||
.addNativeModule(new AndroidInfoModule(getContext()))
|
||||
.addNativeModule(new DeviceInfoModule(getContext()))
|
||||
.addNativeModule(new AppStateModule(getContext()))
|
||||
.addNativeModule(new FakeWebSocketModule())
|
||||
|
||||
@@ -61,7 +61,7 @@ public class ViewRenderingTestCase extends ReactIntegrationTestCase {
|
||||
|
||||
mCatalystInstance = ReactTestHelper.catalystInstanceBuilder(this)
|
||||
.addNativeModule(uiManager)
|
||||
.addNativeModule(new AndroidInfoModule())
|
||||
.addNativeModule(new AndroidInfoModule(getContext()))
|
||||
.addNativeModule(new DeviceInfoModule(getContext()))
|
||||
.addNativeModule(new AppStateModule(getContext()))
|
||||
.addNativeModule(new FakeWebSocketModule())
|
||||
|
||||
@@ -79,7 +79,7 @@ import javax.inject.Provider;
|
||||
new Provider<NativeModule>() {
|
||||
@Override
|
||||
public NativeModule get() {
|
||||
return new AndroidInfoModule();
|
||||
return new AndroidInfoModule(reactContext);
|
||||
}
|
||||
}),
|
||||
ModuleSpec.nativeModuleSpec(
|
||||
|
||||
@@ -75,11 +75,21 @@ public abstract class ReactActivity extends Activity
|
||||
mDelegate.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
return mDelegate.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
return mDelegate.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
|
||||
return mDelegate.onKeyLongPress(keyCode, event) || super.onKeyLongPress(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (!mDelegate.onBackPressed()) {
|
||||
|
||||
@@ -127,6 +127,16 @@ public class ReactActivityDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (getReactNativeHost().hasInstance()
|
||||
&& getReactNativeHost().getUseDeveloperSupport()
|
||||
&& keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
|
||||
event.startTracking();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU) {
|
||||
@@ -143,6 +153,16 @@ public class ReactActivityDelegate {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
|
||||
if (getReactNativeHost().hasInstance()
|
||||
&& getReactNativeHost().getUseDeveloperSupport()
|
||||
&& keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
|
||||
getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean onBackPressed() {
|
||||
if (getReactNativeHost().hasInstance()) {
|
||||
getReactNativeHost().getReactInstanceManager().onBackPressed();
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package com.facebook.react;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Responsible for dispatching events specific for hardware inputs.
|
||||
*/
|
||||
public class ReactAndroidHWInputDeviceHelper {
|
||||
|
||||
/**
|
||||
* Contains a mapping between handled KeyEvents and the corresponding navigation event
|
||||
* that should be fired when the KeyEvent is received.
|
||||
*/
|
||||
private static final Map<Integer, String> KEY_EVENTS_ACTIONS = MapBuilder.of(
|
||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||
"select",
|
||||
KeyEvent.KEYCODE_ENTER,
|
||||
"select",
|
||||
KeyEvent.KEYCODE_SPACE,
|
||||
"select",
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
|
||||
"playPause",
|
||||
KeyEvent.KEYCODE_MEDIA_REWIND,
|
||||
"rewind",
|
||||
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
|
||||
"fastForward"
|
||||
);
|
||||
|
||||
/**
|
||||
* We keep a reference to the last focused view id
|
||||
* so that we can send it as a target for key events
|
||||
* and be able to send a blur event when focus changes.
|
||||
*/
|
||||
private int mLastFocusedViewId = View.NO_ID;
|
||||
|
||||
private final ReactRootView mReactRootView;
|
||||
|
||||
ReactAndroidHWInputDeviceHelper(ReactRootView mReactRootView) {
|
||||
this.mReactRootView = mReactRootView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link ReactRootView}.
|
||||
* This is the main place the key events are handled.
|
||||
*/
|
||||
public void handleKeyEvent(KeyEvent ev) {
|
||||
int eventKeyCode = ev.getKeyCode();
|
||||
int eventKeyAction = ev.getAction();
|
||||
if (eventKeyAction == KeyEvent.ACTION_UP && KEY_EVENTS_ACTIONS.containsKey(eventKeyCode)) {
|
||||
dispatchEvent(KEY_EVENTS_ACTIONS.get(eventKeyCode), mLastFocusedViewId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link ReactRootView} when focused view changes.
|
||||
*/
|
||||
public void onFocusChanged(View newFocusedView) {
|
||||
if (mLastFocusedViewId == newFocusedView.getId()) {
|
||||
return;
|
||||
}
|
||||
if (mLastFocusedViewId != View.NO_ID) {
|
||||
dispatchEvent("blur", mLastFocusedViewId);
|
||||
}
|
||||
mLastFocusedViewId = newFocusedView.getId();
|
||||
dispatchEvent("focus", newFocusedView.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link ReactRootView} when the whole view hierarchy looses focus.
|
||||
*/
|
||||
public void clearFocus() {
|
||||
if (mLastFocusedViewId != View.NO_ID) {
|
||||
dispatchEvent("blur", mLastFocusedViewId);
|
||||
}
|
||||
mLastFocusedViewId = View.NO_ID;
|
||||
}
|
||||
|
||||
private void dispatchEvent(String eventType, int targetViewId) {
|
||||
WritableMap event = new WritableNativeMap();
|
||||
event.putString("eventType", eventType);
|
||||
if (targetViewId != View.NO_ID) {
|
||||
event.putInt("tag", targetViewId);
|
||||
}
|
||||
mReactRootView.sendEvent("onHWKeyEvent", event);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
@@ -84,6 +85,7 @@ public class ReactRootView extends SizeMonitoringFrameLayout
|
||||
private boolean mIsAttachedToInstance;
|
||||
private boolean mShouldLogContentAppeared;
|
||||
private final JSTouchDispatcher mJSTouchDispatcher = new JSTouchDispatcher(this);
|
||||
private final ReactAndroidHWInputDeviceHelper mAndroidHWInputDeviceHelper = new ReactAndroidHWInputDeviceHelper(this);
|
||||
private boolean mWasMeasured = false;
|
||||
private int mWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
|
||||
private int mHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
|
||||
@@ -213,6 +215,47 @@ public class ReactRootView extends SizeMonitoringFrameLayout
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent ev) {
|
||||
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
||||
mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Unable to handle key event as the catalyst instance has not been attached");
|
||||
return super.dispatchKeyEvent(ev);
|
||||
}
|
||||
mAndroidHWInputDeviceHelper.handleKeyEvent(ev);
|
||||
return super.dispatchKeyEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
||||
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
||||
mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Unable to handle focus changed event as the catalyst instance has not been attached");
|
||||
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
return;
|
||||
}
|
||||
mAndroidHWInputDeviceHelper.clearFocus();
|
||||
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestChildFocus(View child, View focused) {
|
||||
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
||||
mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
FLog.w(
|
||||
ReactConstants.TAG,
|
||||
"Unable to handle child focus changed event as the catalyst instance has not been attached");
|
||||
super.requestChildFocus(child, focused);
|
||||
return;
|
||||
}
|
||||
mAndroidHWInputDeviceHelper.onFocusChanged(focused);
|
||||
super.requestChildFocus(child, focused);
|
||||
}
|
||||
|
||||
private void dispatchJSTouchEvent(MotionEvent event) {
|
||||
if (mReactInstanceManager == null || !mIsAttachedToInstance ||
|
||||
mReactInstanceManager.getCurrentReactContext() == null) {
|
||||
@@ -536,6 +579,14 @@ public class ReactRootView extends SizeMonitoringFrameLayout
|
||||
public ReactInstanceManager getReactInstanceManager() {
|
||||
return mReactInstanceManager;
|
||||
}
|
||||
|
||||
/* package */ void sendEvent(String eventName, @Nullable WritableMap params) {
|
||||
if (mReactInstanceManager != null) {
|
||||
mReactInstanceManager.getCurrentReactContext()
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener {
|
||||
private final Rect mVisibleViewArea;
|
||||
@@ -665,13 +716,5 @@ public class ReactRootView extends SizeMonitoringFrameLayout
|
||||
.getNativeModule(DeviceInfoModule.class)
|
||||
.emitUpdateDimensionsEvent();
|
||||
}
|
||||
|
||||
private void sendEvent(String eventName, @Nullable WritableMap params) {
|
||||
if (mReactInstanceManager != null) {
|
||||
mReactInstanceManager.getCurrentReactContext()
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
|
||||
package com.facebook.react.modules.systeminfo;
|
||||
|
||||
import android.app.UiModeManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
|
||||
import com.facebook.react.bridge.BaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.module.annotations.ReactModule;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -17,14 +20,41 @@ import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static android.content.Context.UI_MODE_SERVICE;
|
||||
|
||||
/**
|
||||
* Module that exposes Android Constants to JS.
|
||||
*/
|
||||
@ReactModule(name = "PlatformConstants")
|
||||
public class AndroidInfoModule extends BaseJavaModule {
|
||||
public class AndroidInfoModule extends ReactContextBaseJavaModule {
|
||||
|
||||
private static final String IS_TESTING = "IS_TESTING";
|
||||
|
||||
public AndroidInfoModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* See: https://developer.android.com/reference/android/app/UiModeManager.html#getCurrentModeType()
|
||||
*/
|
||||
private String uiMode() {
|
||||
UiModeManager uiModeManager = (UiModeManager) getReactApplicationContext().getSystemService(UI_MODE_SERVICE);
|
||||
switch (uiModeManager.getCurrentModeType()) {
|
||||
case Configuration.UI_MODE_TYPE_TELEVISION:
|
||||
return "tv";
|
||||
case Configuration.UI_MODE_TYPE_CAR:
|
||||
return "car";
|
||||
case Configuration.UI_MODE_TYPE_DESK:
|
||||
return "desk";
|
||||
case Configuration.UI_MODE_TYPE_WATCH:
|
||||
return "watch";
|
||||
case Configuration.UI_MODE_TYPE_NORMAL:
|
||||
return "normal";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "PlatformConstants";
|
||||
@@ -41,6 +71,7 @@ public class AndroidInfoModule extends BaseJavaModule {
|
||||
constants.put("ServerHost", AndroidInfoHelpers.getServerHost());
|
||||
constants.put("isTesting", "true".equals(System.getProperty(IS_TESTING)));
|
||||
constants.put("reactNativeVersion", ReactNativeVersion.VERSION);
|
||||
constants.put("uiMode", uiMode());
|
||||
return constants;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,8 +56,16 @@ public class ReactViewManager extends ViewGroupManager<ReactViewGroup> {
|
||||
view.setFocusable(accessible);
|
||||
}
|
||||
|
||||
@ReactPropGroup(
|
||||
names = {
|
||||
@ReactProp(name = "hasTVPreferredFocus")
|
||||
public void setTVPreferredFocus(ReactViewGroup view, boolean hasTVPreferredFocus) {
|
||||
if (hasTVPreferredFocus) {
|
||||
view.setFocusable(true);
|
||||
view.setFocusableInTouchMode(true);
|
||||
view.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactPropGroup(names = {
|
||||
ViewProps.BORDER_RADIUS,
|
||||
ViewProps.BORDER_TOP_LEFT_RADIUS,
|
||||
ViewProps.BORDER_TOP_RIGHT_RADIUS,
|
||||
|
||||
Reference in New Issue
Block a user