Apple TV support 4: support for input (tvOS focus engine)

Reviewed By: shergin

Differential Revision: D4333546

fbshipit-source-id: 8655070e81dbb62a80ab1f00a43ef6c2d9654618
This commit is contained in:
Pieter De Baets 2016-12-19 06:26:07 -08:00 committed by Facebook Github Bot
parent 2cc587f3f4
commit c92ad5f6ae
46 changed files with 1012 additions and 35 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

@ -30,6 +30,7 @@
- (void)setUp - (void)setUp
{ {
_runner = RCTInitRunnerForApp(@"IntegrationTests/IntegrationTestsApp", nil); _runner = RCTInitRunnerForApp(@"IntegrationTests/IntegrationTestsApp", nil);
_runner.recordMode = NO;
} }
#pragma mark - Test harness #pragma mark - Test harness

View File

@ -41,12 +41,14 @@
[_runner runTest:_cmd module:@#name]; \ [_runner runTest:_cmd module:@#name]; \
} }
#if !TARGET_OS_TV // None of these will run in tvOS due to StatusBar not existing
RCT_TEST(ViewExample) RCT_TEST(ViewExample)
RCT_TEST(LayoutExample) RCT_TEST(LayoutExample)
RCT_TEST(TextExample) RCT_TEST(TextExample)
#if !TARGET_OS_TV
// No switch or slider available on tvOS
RCT_TEST(SwitchExample) RCT_TEST(SwitchExample)
RCT_TEST(SliderExample) RCT_TEST(SliderExample)
// TabBarExample on tvOS passes locally but not on Travis
RCT_TEST(TabBarExample) RCT_TEST(TabBarExample)
#endif #endif

View File

@ -213,7 +213,10 @@ RCT_EXPORT_METHOD(test:(__unused NSString *)a
(void)rootView; (void)rootView;
} }
#if !TARGET_OS_TV // userInteractionEnabled is true for Apple TV views
XCTAssertFalse(rootContentView.userInteractionEnabled, @"RCTContentView should have been invalidated"); XCTAssertFalse(rootContentView.userInteractionEnabled, @"RCTContentView should have been invalidated");
#endif
} }
- (void)testUnderlyingBridgeIsDeallocated - (void)testUnderlyingBridgeIsDeallocated

View File

@ -22,6 +22,7 @@
*/ */
'use strict'; 'use strict';
const Platform = require('Platform');
var React = require('react'); var React = require('react');
var ReactNative = require('react-native'); var ReactNative = require('react-native');
var { var {
@ -190,10 +191,10 @@ exports.examples = [
render: function() { render: function() {
return ( return (
<View> <View>
<Text style={{fontFamily: 'Cochin'}}> <Text style={{fontFamily: (Platform.isTVOS ? 'Times' : 'Cochin')}}>
Cochin Cochin
</Text> </Text>
<Text style={{fontFamily: 'Cochin', fontWeight: 'bold'}}> <Text style={{fontFamily: (Platform.isTVOS ? 'Times' : 'Cochin'), fontWeight: 'bold'}}>
Cochin bold Cochin bold
</Text> </Text>
<Text style={{fontFamily: 'Helvetica'}}> <Text style={{fontFamily: 'Helvetica'}}>
@ -202,10 +203,10 @@ exports.examples = [
<Text style={{fontFamily: 'Helvetica', fontWeight: 'bold'}}> <Text style={{fontFamily: 'Helvetica', fontWeight: 'bold'}}>
Helvetica bold Helvetica bold
</Text> </Text>
<Text style={{fontFamily: 'Verdana'}}> <Text style={{fontFamily: (Platform.isTVOS ? 'Courier' : 'Verdana')}}>
Verdana Verdana
</Text> </Text>
<Text style={{fontFamily: 'Verdana', fontWeight: 'bold'}}> <Text style={{fontFamily: (Platform.isTVOS ? 'Courier' : 'Verdana'), fontWeight: 'bold'}}>
Verdana bold Verdana bold
</Text> </Text>
</View> </View>
@ -565,10 +566,10 @@ exports.examples = [
<Text style={{fontVariant: ['small-caps']}}> <Text style={{fontVariant: ['small-caps']}}>
Small Caps{'\n'} Small Caps{'\n'}
</Text> </Text>
<Text style={{fontFamily: 'Hoefler Text', fontVariant: ['oldstyle-nums']}}> <Text style={{fontFamily: (Platform.isTVOS ? 'Times' : 'Hoefler Text'), fontVariant: ['oldstyle-nums']}}>
Old Style nums 0123456789{'\n'} Old Style nums 0123456789{'\n'}
</Text> </Text>
<Text style={{fontFamily: 'Hoefler Text', fontVariant: ['lining-nums']}}> <Text style={{fontFamily: (Platform.isTVOS ? 'Times' : 'Hoefler Text'), fontVariant: ['lining-nums']}}>
Lining nums 0123456789{'\n'} Lining nums 0123456789{'\n'}
</Text> </Text>
<Text style={{fontVariant: ['tabular-nums']}}> <Text style={{fontVariant: ['tabular-nums']}}>

View File

@ -23,23 +23,22 @@
'use strict'; 'use strict';
const ListView = require('ListView'); const ListView = require('ListView');
const Platform = require('Platform');
const React = require('react'); const React = require('react');
const StyleSheet = require('StyleSheet'); const StyleSheet = require('StyleSheet');
const Text = require('Text'); const Text = require('Text');
const TextInput = require('TextInput'); const TextInput = require('TextInput');
const TouchableHighlight = require('TouchableHighlight'); const TouchableHighlight = require('TouchableHighlight');
const View = require('View');
const UIExplorerActions = require('./UIExplorerActions'); const UIExplorerActions = require('./UIExplorerActions');
const UIExplorerStatePersister = require('./UIExplorerStatePersister'); const UIExplorerStatePersister = require('./UIExplorerStatePersister');
const View = require('View');
import type { import type {
UIExplorerExample, UIExplorerExample,
} from './UIExplorerList.ios'; } from './UIExplorerList.ios';
import type { import type {
PassProps, PassProps,
} from './UIExplorerStatePersister'; } from './UIExplorerStatePersister';
import type { import type {
StyleObj, StyleObj,
} from 'StyleSheetTypes'; } from 'StyleSheetTypes';
@ -66,7 +65,7 @@ class UIExplorerExampleList extends React.Component {
render(): ?React.Element<any> { render(): ?React.Element<any> {
const filterText = this.props.persister.state.filter; const filterText = this.props.persister.state.filter;
const filterRegex = new RegExp(String(filterText), 'i'); const filterRegex = new RegExp(String(filterText), 'i');
const filter = (example) => filterRegex.test(example.module.title); const filter = (example) => filterRegex.test(example.module.title) && (!Platform.isTVOS || example.supportsTVOS);
const dataSource = ds.cloneWithRowsAndSections({ const dataSource = ds.cloneWithRowsAndSections({
components: this.props.list.ComponentExamples.filter(filter), components: this.props.list.ComponentExamples.filter(filter),

View File

@ -25,132 +25,164 @@
export type UIExplorerExample = { export type UIExplorerExample = {
key: string, key: string,
module: Object, module: Object,
supportsTVOS: boolean
}; };
const ComponentExamples: Array<UIExplorerExample> = [ const ComponentExamples: Array<UIExplorerExample> = [
{ {
key: 'ActivityIndicatorExample', key: 'ActivityIndicatorExample',
module: require('./ActivityIndicatorExample'), module: require('./ActivityIndicatorExample'),
supportsTVOS: true,
}, },
{ {
key: 'ButtonExample', key: 'ButtonExample',
module: require('./ButtonExample'), module: require('./ButtonExample'),
supportsTVOS: true,
}, },
{ {
key: 'DatePickerIOSExample', key: 'DatePickerIOSExample',
module: require('./DatePickerIOSExample'), module: require('./DatePickerIOSExample'),
supportsTVOS: false,
}, },
{ {
key: 'ImageExample', key: 'ImageExample',
module: require('./ImageExample'), module: require('./ImageExample'),
supportsTVOS: true,
}, },
{ {
key: 'KeyboardAvoidingViewExample', key: 'KeyboardAvoidingViewExample',
module: require('./KeyboardAvoidingViewExample'), module: require('./KeyboardAvoidingViewExample'),
supportsTVOS: false,
}, },
{ {
key: 'LayoutEventsExample', key: 'LayoutEventsExample',
module: require('./LayoutEventsExample'), module: require('./LayoutEventsExample'),
supportsTVOS: true,
}, },
{ {
key: 'ListViewExample', key: 'ListViewExample',
module: require('./ListViewExample'), module: require('./ListViewExample'),
supportsTVOS: true,
}, },
{ {
key: 'ListViewGridLayoutExample', key: 'ListViewGridLayoutExample',
module: require('./ListViewGridLayoutExample'), module: require('./ListViewGridLayoutExample'),
supportsTVOS: true,
}, },
{ {
key: 'ListViewPagingExample', key: 'ListViewPagingExample',
module: require('./ListViewPagingExample'), module: require('./ListViewPagingExample'),
supportsTVOS: true,
}, },
{ {
key: 'MapViewExample', key: 'MapViewExample',
module: require('./MapViewExample'), module: require('./MapViewExample'),
supportsTVOS: true,
}, },
{ {
key: 'ModalExample', key: 'ModalExample',
module: require('./ModalExample'), module: require('./ModalExample'),
supportsTVOS: true,
}, },
{ {
key: 'NavigatorExample', key: 'NavigatorExample',
module: require('./Navigator/NavigatorExample'), module: require('./Navigator/NavigatorExample'),
supportsTVOS: true,
}, },
{ {
key: 'NavigatorIOSColorsExample', key: 'NavigatorIOSColorsExample',
module: require('./NavigatorIOSColorsExample'), module: require('./NavigatorIOSColorsExample'),
supportsTVOS: false,
}, },
{ {
key: 'NavigatorIOSExample', key: 'NavigatorIOSExample',
module: require('./NavigatorIOSExample'), module: require('./NavigatorIOSExample'),
supportsTVOS: true,
}, },
{ {
key: 'PickerExample', key: 'PickerExample',
module: require('./PickerExample'), module: require('./PickerExample'),
supportsTVOS: false,
}, },
{ {
key: 'PickerIOSExample', key: 'PickerIOSExample',
module: require('./PickerIOSExample'), module: require('./PickerIOSExample'),
supportsTVOS: false,
}, },
{ {
key: 'ProgressViewIOSExample', key: 'ProgressViewIOSExample',
module: require('./ProgressViewIOSExample'), module: require('./ProgressViewIOSExample'),
supportsTVOS: true,
}, },
{ {
key: 'RefreshControlExample', key: 'RefreshControlExample',
module: require('./RefreshControlExample'), module: require('./RefreshControlExample'),
supportsTVOS: false,
}, },
{ {
key: 'ScrollViewExample', key: 'ScrollViewExample',
module: require('./ScrollViewExample'), module: require('./ScrollViewExample'),
supportsTVOS: true,
}, },
{ {
key: 'SegmentedControlIOSExample', key: 'SegmentedControlIOSExample',
module: require('./SegmentedControlIOSExample'), module: require('./SegmentedControlIOSExample'),
supportsTVOS: false,
}, },
{ {
key: 'SliderExample', key: 'SliderExample',
module: require('./SliderExample'), module: require('./SliderExample'),
supportsTVOS: false,
}, },
{ {
key: 'StatusBarExample', key: 'StatusBarExample',
module: require('./StatusBarExample'), module: require('./StatusBarExample'),
supportsTVOS: false,
}, },
{ {
key: 'SwipeableListViewExample', key: 'SwipeableListViewExample',
module: require('./SwipeableListViewExample') module: require('./SwipeableListViewExample'),
supportsTVOS: false,
}, },
{ {
key: 'SwitchExample', key: 'SwitchExample',
module: require('./SwitchExample'), module: require('./SwitchExample'),
supportsTVOS: false,
}, },
{ {
key: 'TabBarIOSExample', key: 'TabBarIOSExample',
module: require('./TabBarIOSExample'), module: require('./TabBarIOSExample'),
supportsTVOS: true,
}, },
{ {
key: 'TextExample', key: 'TextExample',
module: require('./TextExample.ios'), module: require('./TextExample.ios'),
supportsTVOS: true,
}, },
{ {
key: 'TextInputExample', key: 'TextInputExample',
module: require('./TextInputExample.ios'), module: require('./TextInputExample.ios'),
supportsTVOS: true,
}, },
{ {
key: 'TouchableExample', key: 'TouchableExample',
module: require('./TouchableExample'), module: require('./TouchableExample'),
supportsTVOS: false,
}, },
{ {
key: 'TransparentHitTestExample', key: 'TransparentHitTestExample',
module: require('./TransparentHitTestExample'), module: require('./TransparentHitTestExample'),
supportsTVOS: false,
}, },
{ {
key: 'ViewExample', key: 'ViewExample',
module: require('./ViewExample'), module: require('./ViewExample'),
supportsTVOS: true,
}, },
{ {
key: 'WebViewExample', key: 'WebViewExample',
module: require('./WebViewExample'), module: require('./WebViewExample'),
supportsTVOS: false,
}, },
]; ];
@ -158,138 +190,172 @@ const APIExamples: Array<UIExplorerExample> = [
{ {
key: 'AccessibilityIOSExample', key: 'AccessibilityIOSExample',
module: require('./AccessibilityIOSExample'), module: require('./AccessibilityIOSExample'),
supportsTVOS: false,
}, },
{ {
key: 'ActionSheetIOSExample', key: 'ActionSheetIOSExample',
module: require('./ActionSheetIOSExample'), module: require('./ActionSheetIOSExample'),
supportsTVOS: true,
}, },
{ {
key: 'AdSupportIOSExample', key: 'AdSupportIOSExample',
module: require('./AdSupportIOSExample'), module: require('./AdSupportIOSExample'),
supportsTVOS: false,
}, },
{ {
key: 'AlertExample', key: 'AlertExample',
module: require('./AlertExample').AlertExample, module: require('./AlertExample').AlertExample,
supportsTVOS: true,
}, },
{ {
key: 'AlertIOSExample', key: 'AlertIOSExample',
module: require('./AlertIOSExample'), module: require('./AlertIOSExample'),
supportsTVOS: true,
}, },
{ {
key: 'AnimatedExample', key: 'AnimatedExample',
module: require('./AnimatedExample'), module: require('./AnimatedExample'),
supportsTVOS: true,
}, },
{ {
key: 'AnExApp', key: 'AnExApp',
module: require('./AnimatedGratuitousApp/AnExApp'), module: require('./AnimatedGratuitousApp/AnExApp'),
supportsTVOS: true,
}, },
{ {
key: 'AppStateExample', key: 'AppStateExample',
module: require('./AppStateExample'), module: require('./AppStateExample'),
supportsTVOS: true,
}, },
{ {
key: 'AsyncStorageExample', key: 'AsyncStorageExample',
module: require('./AsyncStorageExample'), module: require('./AsyncStorageExample'),
supportsTVOS: true,
}, },
{ {
key: 'BorderExample', key: 'BorderExample',
module: require('./BorderExample'), module: require('./BorderExample'),
supportsTVOS: true,
}, },
{ {
key: 'BoxShadowExample', key: 'BoxShadowExample',
module: require('./BoxShadowExample'), module: require('./BoxShadowExample'),
supportsTVOS: true,
}, },
{ {
key: 'CameraRollExample', key: 'CameraRollExample',
module: require('./CameraRollExample'), module: require('./CameraRollExample'),
supportsTVOS: false,
}, },
{ {
key: 'ClipboardExample', key: 'ClipboardExample',
module: require('./ClipboardExample'), module: require('./ClipboardExample'),
supportsTVOS: false,
}, },
{ {
key: 'GeolocationExample', key: 'GeolocationExample',
module: require('./GeolocationExample'), module: require('./GeolocationExample'),
supportsTVOS: false,
}, },
{ {
key: 'ImageEditingExample', key: 'ImageEditingExample',
module: require('./ImageEditingExample'), module: require('./ImageEditingExample'),
supportsTVOS: false,
}, },
{ {
key: 'LayoutAnimationExample', key: 'LayoutAnimationExample',
module: require('./LayoutAnimationExample'), module: require('./LayoutAnimationExample'),
supportsTVOS: true,
}, },
{ {
key: 'LayoutExample', key: 'LayoutExample',
module: require('./LayoutExample'), module: require('./LayoutExample'),
supportsTVOS: true,
}, },
{ {
key: 'LinkingExample', key: 'LinkingExample',
module: require('./LinkingExample'), module: require('./LinkingExample'),
supportsTVOS: true,
}, },
{ {
key: 'NativeAnimationsExample', key: 'NativeAnimationsExample',
module: require('./NativeAnimationsExample'), module: require('./NativeAnimationsExample'),
supportsTVOS: true,
}, },
{ {
key: 'NavigationExperimentalExample', key: 'NavigationExperimentalExample',
module: require('./NavigationExperimental/NavigationExperimentalExample'), module: require('./NavigationExperimental/NavigationExperimentalExample'),
supportsTVOS: true,
}, },
{ {
key: 'NetInfoExample', key: 'NetInfoExample',
module: require('./NetInfoExample'), module: require('./NetInfoExample'),
supportsTVOS: true,
}, },
{ {
key: 'OrientationChangeExample', key: 'OrientationChangeExample',
module: require('./OrientationChangeExample'), module: require('./OrientationChangeExample'),
supportsTVOS: false,
}, },
{ {
key: 'PanResponderExample', key: 'PanResponderExample',
module: require('./PanResponderExample'), module: require('./PanResponderExample'),
supportsTVOS: false,
}, },
{ {
key: 'PointerEventsExample', key: 'PointerEventsExample',
module: require('./PointerEventsExample'), module: require('./PointerEventsExample'),
supportsTVOS: false,
}, },
{ {
key: 'PushNotificationIOSExample', key: 'PushNotificationIOSExample',
module: require('./PushNotificationIOSExample'), module: require('./PushNotificationIOSExample'),
supportsTVOS: false,
}, },
{ {
key: 'RCTRootViewIOSExample', key: 'RCTRootViewIOSExample',
module: require('./RCTRootViewIOSExample'), module: require('./RCTRootViewIOSExample'),
supportsTVOS: true,
}, },
{ {
key: 'RTLExample', key: 'RTLExample',
module: require('./RTLExample'), module: require('./RTLExample'),
supportsTVOS: true,
}, },
{ {
key: 'ShareExample', key: 'ShareExample',
module: require('./ShareExample'), module: require('./ShareExample'),
supportsTVOS: true,
}, },
{ {
key: 'SnapshotExample', key: 'SnapshotExample',
module: require('./SnapshotExample'), module: require('./SnapshotExample'),
supportsTVOS: true,
}, },
{ {
key: 'TimerExample', key: 'TimerExample',
module: require('./TimerExample'), module: require('./TimerExample'),
supportsTVOS: true,
}, },
{ {
key: 'TransformExample', key: 'TransformExample',
module: require('./TransformExample'), module: require('./TransformExample'),
supportsTVOS: true,
}, },
{ {
key: 'VibrationExample', key: 'VibrationExample',
module: require('./VibrationExample'), module: require('./VibrationExample'),
supportsTVOS: false,
}, },
{ {
key: 'WebSocketExample', key: 'WebSocketExample',
module: require('./WebSocketExample'), module: require('./WebSocketExample'),
supportsTVOS: true,
}, },
{ {
key: 'XHRExample', key: 'XHRExample',
module: require('./XHRExample.ios'), module: require('./XHRExample.ios'),
supportsTVOS: true,
}, },
]; ];

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) 2016-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.
*
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
* all intellectual property and other proprietary rights, in and to the React
* Native CustomComponents software (the "Software"). Subject to your
* compliance with these terms, you are hereby granted a non-exclusive,
* worldwide, royalty-free copyright license to (1) use and copy the Software;
* and (2) reproduce and distribute the Software as part of your own software
* ("Your Software"). Facebook reserves all rights not expressly granted to
* you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @providesModule TVEventHandler
* @flow
*/
'use strict';
function TVEventHandler() {}
TVEventHandler.prototype.enable = function(component: ?any, callback: Function) {};
TVEventHandler.prototype.disable = function() {};
module.exports = TVEventHandler;

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2016-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.
*
* Facebook, Inc. ("Facebook") owns all right, title and interest, including
* all intellectual property and other proprietary rights, in and to the React
* Native CustomComponents software (the "Software"). Subject to your
* compliance with these terms, you are hereby granted a non-exclusive,
* worldwide, royalty-free copyright license to (1) use and copy the Software;
* and (2) reproduce and distribute the Software as part of your own software
* ("Your Software"). Facebook reserves all rights not expressly granted to
* you in this license agreement.
*
* THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED.
* IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR
* EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @providesModule TVEventHandler
* @flow
*/
'use strict';
const React = require('React');
const TVNavigationEventEmitter = require('NativeModules').TVNavigationEventEmitter;
const NativeEventEmitter = require('NativeEventEmitter');
function TVEventHandler() {
this.__nativeTVNavigationEventListener = null;
this.__nativeTVNavigationEventEmitter = null;
}
TVEventHandler.prototype.enable = function(component: ?any, callback: Function) {
if (!TVNavigationEventEmitter) {
return;
}
this.__nativeTVNavigationEventEmitter = new NativeEventEmitter(TVNavigationEventEmitter);
this.__nativeTVNavigationEventListener = this.__nativeTVNavigationEventEmitter.addListener(
'onTVNavEvent',
(data) => {
if (callback) {
callback(component, data);
}
}
);
};
TVEventHandler.prototype.disable = function() {
if (this.__nativeTVNavigationEventListener) {
this.__nativeTVNavigationEventListener.remove();
delete this.__nativeTVNavigationEventListener;
}
if (this.__nativeTVNavigationEventEmitter) {
delete this.__nativeTVNavigationEventEmitter;
}
};
module.exports = TVEventHandler;

View File

@ -0,0 +1,77 @@
/**
* 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.
*
* @providesModule TVViewPropTypes
* @flow
*/
'use strict';
var PropTypes = require('React').PropTypes;
/**
* Additional View properties for Apple TV
*/
var TVViewPropTypes = {
/**
* *(Apple TV only)* When set to true, this view will be focusable
* and navigable using the Apple TV remote.
*
* @platform ios
*/
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
*/
hasTVPreferredFocus: PropTypes.bool,
/**
* *(Apple TV only)* Object with properties to control Apple TV parallax effects.
*
* enabled: If true, parallax effects are enabled. Defaults to true.
* shiftDistanceX: Defaults to 2.0.
* shiftDistanceY: Defaults to 2.0.
* tiltAngle: Defaults to 0.05.
* magnification: Defaults to 1.0.
*
* @platform ios
*/
tvParallaxProperties: PropTypes.object,
/**
* *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 2.0.
*
* @platform ios
*/
tvParallaxShiftDistanceX: PropTypes.number,
/**
* *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 2.0.
*
* @platform ios
*/
tvParallaxShiftDistanceY: PropTypes.number,
/**
* *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 0.05.
*
* @platform ios
*/
tvParallaxTiltAngle: PropTypes.number,
/**
* *(Apple TV only)* May be used to change the appearance of the Apple TV parallax effect when this view goes in or out of focus. Defaults to 1.0.
*
* @platform ios
*/
tvParallaxMagnification: PropTypes.number,
};
module.exports = TVViewPropTypes;

View File

@ -19,6 +19,7 @@ var React = require('React');
var ReactNative = require('ReactNative'); var ReactNative = require('ReactNative');
var StaticContainer = require('StaticContainer.react'); var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet'); var StyleSheet = require('StyleSheet');
var TVEventHandler = require('TVEventHandler');
var View = require('View'); var View = require('View');
var invariant = require('fbjs/lib/invariant'); var invariant = require('fbjs/lib/invariant');
@ -37,13 +38,13 @@ function getuid() {
} }
class NavigatorTransitionerIOS extends React.Component { class NavigatorTransitionerIOS extends React.Component {
requestSchedulingNavigation = (cb) => { requestSchedulingNavigation(cb) {
RCTNavigatorManager.requestSchedulingJavaScriptNavigation( RCTNavigatorManager.requestSchedulingJavaScriptNavigation(
ReactNative.findNodeHandle(this), ReactNative.findNodeHandle(this),
logError, logError,
cb cb
); );
}; }
render() { render() {
return ( return (
@ -89,11 +90,11 @@ type Route = {
backButtonIcon?: Object, backButtonIcon?: Object,
leftButtonTitle?: string, leftButtonTitle?: string,
leftButtonIcon?: Object, leftButtonIcon?: Object,
leftButtonSystemIcon?: SystemButtonType; leftButtonSystemIcon?: SystemButtonType,
onLeftButtonPress?: Function, onLeftButtonPress?: Function,
rightButtonTitle?: string, rightButtonTitle?: string,
rightButtonIcon?: Object, rightButtonIcon?: Object,
rightButtonSystemIcon?: SystemButtonType; rightButtonSystemIcon?: SystemButtonType,
onRightButtonPress?: Function, onRightButtonPress?: Function,
wrapperStyle?: any, wrapperStyle?: any,
}; };
@ -519,11 +520,13 @@ var NavigatorIOS = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._emitDidFocus(this.state.routeStack[this.state.observedTopOfStack]); this._emitDidFocus(this.state.routeStack[this.state.observedTopOfStack]);
this._enableTVEventHandler();
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
this.navigationContext.dispose(); this.navigationContext.dispose();
this.navigationContext = new NavigationContext(); this.navigationContext = new NavigationContext();
this._disableTVEventHandler();
}, },
getDefaultProps: function(): Object { getDefaultProps: function(): Object {
@ -891,6 +894,24 @@ var NavigatorIOS = React.createClass({
); );
}, },
_tvEventHandler: (undefined: ?TVEventHandler),
_enableTVEventHandler: function() {
this._tvEventHandler = new TVEventHandler();
this._tvEventHandler.enable(this, function(cmp, evt) {
if (evt && evt.eventType === 'menu') {
cmp.pop();
}
});
},
_disableTVEventHandler: function() {
if (this._tvEventHandler) {
this._tvEventHandler.disable();
delete this._tvEventHandler;
}
},
render: function() { render: function() {
return ( return (
<View style={this.props.style}> <View style={this.props.style}>

View File

@ -11,12 +11,12 @@
*/ */
'use strict'; 'use strict';
var ColorPropType = require('ColorPropType');
var Image = require('Image'); var Image = require('Image');
var React = require('React'); var React = require('React');
var StaticContainer = require('StaticContainer.react'); var StaticContainer = require('StaticContainer.react');
var StyleSheet = require('StyleSheet'); var StyleSheet = require('StyleSheet');
var View = require('View'); var View = require('View');
var ColorPropType = require('ColorPropType');
var requireNativeComponent = require('requireNativeComponent'); var requireNativeComponent = require('requireNativeComponent');
@ -86,6 +86,13 @@ class TabBarItemIOS extends React.Component {
* is defined. * is defined.
*/ */
title: React.PropTypes.string, title: React.PropTypes.string,
/**
*(Apple TV only)* When set to true, this view will be focusable
* and navigable using the Apple TV remote.
*
* @platform ios
*/
isTVSelectable: React.PropTypes.bool,
}; };
state = { state = {

View File

@ -12,12 +12,15 @@
'use strict'; 'use strict';
const BoundingDimensions = require('BoundingDimensions'); const BoundingDimensions = require('BoundingDimensions');
const Platform = require('Platform');
const Position = require('Position'); const Position = require('Position');
const React = require('React'); // eslint-disable-line no-unused-vars const React = require('React');
const TVEventHandler = require('TVEventHandler');
const TouchEventUtils = require('fbjs/lib/TouchEventUtils'); const TouchEventUtils = require('fbjs/lib/TouchEventUtils');
const UIManager = require('UIManager'); const UIManager = require('UIManager');
const View = require('View'); const View = require('View');
const findNodeHandle = require('findNodeHandle');
const keyMirror = require('fbjs/lib/keyMirror'); const keyMirror = require('fbjs/lib/keyMirror');
const normalizeColor = require('normalizeColor'); const normalizeColor = require('normalizeColor');
@ -313,10 +316,35 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10;
* @lends Touchable.prototype * @lends Touchable.prototype
*/ */
var TouchableMixin = { var TouchableMixin = {
componentDidMount: function() {
if (!Platform.isTVOS) {
return;
}
this._tvEventHandler = new TVEventHandler();
this._tvEventHandler.enable(this, function(cmp, evt) {
var myTag = findNodeHandle(cmp);
evt.dispatchConfig = {};
if (myTag === evt.tag) {
if (evt.eventType === 'focus') {
cmp.touchableHandleActivePressIn && cmp.touchableHandleActivePressIn(evt);
} else if (evt.eventType === 'blur') {
cmp.touchableHandleActivePressOut && cmp.touchableHandleActivePressOut(evt);
} else if (evt.eventType === 'select') {
cmp.touchableHandlePress && cmp.touchableHandlePress(evt);
}
}
});
},
/** /**
* Clear all timeouts on unmount * Clear all timeouts on unmount
*/ */
componentWillUnmount: function() { componentWillUnmount: function() {
if (this._tvEventHandler) {
this._tvEventHandler.disable();
delete this._tvEventHandler;
}
this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout); this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout);
this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout); this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout); this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);

View File

@ -86,6 +86,25 @@ var TouchableHighlight = React.createClass({
* Called immediately after the underlay is hidden * Called immediately after the underlay is hidden
*/ */
onHideUnderlay: React.PropTypes.func, onHideUnderlay: React.PropTypes.func,
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
*/
hasTVPreferredFocus: React.PropTypes.bool,
/**
* *(Apple TV only)* Object with properties to control Apple TV parallax effects.
*
* enabled: If true, parallax effects are enabled. Defaults to true.
* shiftDistanceX: Defaults to 2.0.
* shiftDistanceY: Defaults to 2.0.
* tiltAngle: Defaults to 0.05.
* magnification: Defaults to 1.0.
*
* @platform ios
*/
tvParallaxProperties: React.PropTypes.object,
}, },
mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin], mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin],
@ -108,7 +127,8 @@ var TouchableHighlight = React.createClass({
underlayStyle: [ underlayStyle: [
INACTIVE_UNDERLAY_PROPS.style, INACTIVE_UNDERLAY_PROPS.style,
props.style, props.style,
] ],
hasTVPreferredFocus: props.hasTVPreferredFocus
}; };
}, },
@ -234,6 +254,9 @@ var TouchableHighlight = React.createClass({
style={this.state.underlayStyle} style={this.state.underlayStyle}
onLayout={this.props.onLayout} onLayout={this.props.onLayout}
hitSlop={this.props.hitSlop} hitSlop={this.props.hitSlop}
isTVSelectable={true}
tvParallaxProperties={this.props.tvParallaxProperties}
hasTVPreferredFocus={this.state.hasTVPreferredFocus}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant} onResponderGrant={this.touchableHandleResponderGrant}

View File

@ -59,11 +59,17 @@ var TouchableOpacity = React.createClass({
* active. Defaults to 0.2. * active. Defaults to 0.2.
*/ */
activeOpacity: React.PropTypes.number, activeOpacity: React.PropTypes.number,
focusedOpacity: React.PropTypes.number,
/**
* Apple TV parallax effects
*/
tvParallaxProperties: React.PropTypes.object,
}, },
getDefaultProps: function() { getDefaultProps: function() {
return { return {
activeOpacity: 0.2, activeOpacity: 0.2,
focusedOpacity: 0.7,
}; };
}, },
@ -156,6 +162,10 @@ var TouchableOpacity = React.createClass({
); );
}, },
_opacityFocused: function() {
this.setOpacityTo(this.props.focusedOpacity);
},
render: function() { render: function() {
return ( return (
<Animated.View <Animated.View
@ -166,6 +176,8 @@ var TouchableOpacity = React.createClass({
style={[this.props.style, {opacity: this.state.anim}]} style={[this.props.style, {opacity: this.state.anim}]}
testID={this.props.testID} testID={this.props.testID}
onLayout={this.props.onLayout} onLayout={this.props.onLayout}
isTVSelectable={true}
tvParallaxProperties={this.props.tvParallaxProperties}
hitSlop={this.props.hitSlop} hitSlop={this.props.hitSlop}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}

View File

@ -4,7 +4,9 @@ exports[`TouchableHighlight renders correctly 1`] = `
accessibilityLabel={undefined} accessibilityLabel={undefined}
accessibilityTraits={undefined} accessibilityTraits={undefined}
accessible={true} accessible={true}
hasTVPreferredFocus={undefined}
hitSlop={undefined} hitSlop={undefined}
isTVSelectable={true}
onLayout={undefined} onLayout={undefined}
onResponderGrant={[Function]} onResponderGrant={[Function]}
onResponderMove={[Function]} onResponderMove={[Function]}
@ -20,7 +22,8 @@ exports[`TouchableHighlight renders correctly 1`] = `
Object {}, Object {},
] ]
} }
testID={undefined}> testID={undefined}
tvParallaxProperties={undefined}>
<Text <Text
accessible={true} accessible={true}
allowFontScaling={true} allowFontScaling={true}

View File

@ -14,12 +14,18 @@
const EdgeInsetsPropType = require('EdgeInsetsPropType'); const EdgeInsetsPropType = require('EdgeInsetsPropType');
const NativeMethodsMixin = require('NativeMethodsMixin'); const NativeMethodsMixin = require('NativeMethodsMixin');
const NativeModules = require('NativeModules'); const NativeModules = require('NativeModules');
const Platform = require('Platform');
const React = require('React'); const React = require('React');
const ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); const ReactNativeStyleAttributes = require('ReactNativeStyleAttributes');
const ReactNativeViewAttributes = require('ReactNativeViewAttributes'); const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
const StyleSheetPropType = require('StyleSheetPropType'); const StyleSheetPropType = require('StyleSheetPropType');
const ViewStylePropTypes = require('ViewStylePropTypes'); const ViewStylePropTypes = require('ViewStylePropTypes');
var TVViewPropTypes = {};
if (Platform.isTVOS) {
TVViewPropTypes = require('TVViewPropTypes');
}
const requireNativeComponent = require('requireNativeComponent'); const requireNativeComponent = require('requireNativeComponent');
const PropTypes = React.PropTypes; const PropTypes = React.PropTypes;
@ -133,6 +139,8 @@ const View = React.createClass({
}, },
propTypes: { propTypes: {
...TVViewPropTypes,
/** /**
* When `true`, indicates that the view is an accessibility element. By default, * When `true`, indicates that the view is an accessibility element. By default,
* all the touchable elements are accessible. * all the touchable elements are accessible.

View File

@ -32,13 +32,14 @@
*/ */
'use strict'; 'use strict';
const React = require('React');
const ReactNative = require('react-native');
const NavigationHeaderTitle = require('NavigationHeaderTitle');
const NavigationHeaderBackButton = require('NavigationHeaderBackButton'); const NavigationHeaderBackButton = require('NavigationHeaderBackButton');
const NavigationPropTypes = require('NavigationPropTypes');
const NavigationHeaderStyleInterpolator = require('NavigationHeaderStyleInterpolator'); const NavigationHeaderStyleInterpolator = require('NavigationHeaderStyleInterpolator');
const NavigationHeaderTitle = require('NavigationHeaderTitle');
const NavigationPropTypes = require('NavigationPropTypes');
const React = require('React');
const ReactComponentWithPureRenderMixin = require('react/lib/ReactComponentWithPureRenderMixin'); const ReactComponentWithPureRenderMixin = require('react/lib/ReactComponentWithPureRenderMixin');
const ReactNative = require('react-native');
const TVEventHandler = require('TVEventHandler');
const { const {
Animated, Animated,
@ -128,6 +129,24 @@ class NavigationHeader extends React.Component<DefaultProps, Props, any> {
); );
} }
_tvEventHandler: TVEventHandler;
componentDidMount(): void {
this._tvEventHandler = new TVEventHandler();
this._tvEventHandler.enable(this, function(cmp, evt) {
if (evt && evt.eventType === 'menu') {
cmp.props.onNavigateBack && cmp.props.onNavigateBack();
}
});
}
componentWillUnmount(): void {
if (this._tvEventHandler) {
this._tvEventHandler.disable();
delete this._tvEventHandler;
}
}
render(): React.Element<any> { render(): React.Element<any> {
const { scenes, style, viewProps } = this.props; const { scenes, style, viewProps } = this.props;
@ -156,32 +175,32 @@ class NavigationHeader extends React.Component<DefaultProps, Props, any> {
); );
} }
_renderLeft(props: NavigationSceneRendererProps): ?React.Element<any> { _renderLeft = (props: NavigationSceneRendererProps): ?React.Element<any> => {
return this._renderSubView( return this._renderSubView(
props, props,
'left', 'left',
this.props.renderLeftComponent, this.props.renderLeftComponent,
NavigationHeaderStyleInterpolator.forLeft, NavigationHeaderStyleInterpolator.forLeft,
); );
} };
_renderTitle(props: NavigationSceneRendererProps): ?React.Element<any> { _renderTitle = (props: NavigationSceneRendererProps): ?React.Element<any> => {
return this._renderSubView( return this._renderSubView(
props, props,
'title', 'title',
this.props.renderTitleComponent, this.props.renderTitleComponent,
NavigationHeaderStyleInterpolator.forCenter, NavigationHeaderStyleInterpolator.forCenter,
); );
} };
_renderRight(props: NavigationSceneRendererProps): ?React.Element<any> { _renderRight = (props: NavigationSceneRendererProps): ?React.Element<any> => {
return this._renderSubView( return this._renderSubView(
props, props,
'right', 'right',
this.props.renderRightComponent, this.props.renderRightComponent,
NavigationHeaderStyleInterpolator.forRight, NavigationHeaderStyleInterpolator.forRight,
); );
} };
_renderSubView( _renderSubView(
props: NavigationSceneRendererProps, props: NavigationSceneRendererProps,

View File

@ -43,6 +43,7 @@ var PanResponder = require('PanResponder');
var React = require('React'); var React = require('React');
var StyleSheet = require('StyleSheet'); var StyleSheet = require('StyleSheet');
var Subscribable = require('Subscribable'); var Subscribable = require('Subscribable');
var TVEventHandler = require('TVEventHandler');
var TimerMixin = require('react-timer-mixin'); var TimerMixin = require('react-timer-mixin');
var View = require('View'); var View = require('View');
@ -472,6 +473,7 @@ var Navigator = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._handleSpringUpdate(); this._handleSpringUpdate();
this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]); this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]);
this._enableTVEventHandler();
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
@ -485,6 +487,8 @@ var Navigator = React.createClass({
if (this._interactionHandle) { if (this._interactionHandle) {
this.clearInteractionHandle(this._interactionHandle); this.clearInteractionHandle(this._interactionHandle);
} }
this._disableTVEventHandler();
}, },
/** /**
@ -1302,6 +1306,24 @@ var Navigator = React.createClass({
}); });
}, },
_tvEventHandler: TVEventHandler,
_enableTVEventHandler: function() {
this._tvEventHandler = new TVEventHandler();
this._tvEventHandler.enable(this, function(cmp, evt) {
if (evt && evt.eventType === 'menu') {
cmp.pop();
}
});
},
_disableTVEventHandler: function() {
if (this._tvEventHandler) {
this._tvEventHandler.disable();
delete this._tvEventHandler;
}
},
render: function() { render: function() {
var newRenderedSceneMap = new Map(); var newRenderedSceneMap = new Map();
var scenes = this.state.routeStack.map((route, index) => { var scenes = this.state.routeStack.map((route, index) => {

View File

@ -112,7 +112,11 @@ expectErrorBlock:(BOOL(^)(NSString *error))expectErrorBlock
launchOptions:nil]; launchOptions:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps];
#if TARGET_OS_TV
rootView.frame = CGRectMake(0, 0, 1920, 1080); // Standard screen size for tvOS
#else
rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices rootView.frame = CGRectMake(0, 0, 320, 2000); // Constant size for testing on multiple devices
#endif
RCTTestModule *testModule = [rootView.bridge moduleForClass:[RCTTestModule class]]; RCTTestModule *testModule = [rootView.bridge moduleForClass:[RCTTestModule class]];
RCTAssert(_testController != nil, @"_testController should not be nil"); RCTAssert(_testController != nil, @"_testController should not be nil");

View File

@ -57,6 +57,16 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
eventCount:_nativeEventCount]; eventCount:_nativeEventCount];
} }
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
[super didUpdateFocusInContext:context withAnimationCoordinator:coordinator];
if(context.nextFocusedView == self) {
_jsRequestingFirstResponder = YES;
} else {
_jsRequestingFirstResponder = NO;
}
}
// This method is overridden for `onKeyPress`. The manager // This method is overridden for `onKeyPress`. The manager
// will not send a keyPress for text that was pasted. // will not send a keyPress for text that was pasted.
- (void)paste:(id)sender - (void)paste:(id)sender

View File

@ -14,8 +14,8 @@
#import <React/RCTFont.h> #import <React/RCTFont.h>
#import <React/RCTShadowView.h> #import <React/RCTShadowView.h>
#import "RCTTextView.h"
#import "RCTConvert+Text.h" #import "RCTConvert+Text.h"
#import "RCTTextView.h"
@implementation RCTTextViewManager @implementation RCTTextViewManager
@ -68,7 +68,10 @@ RCT_CUSTOM_VIEW_PROPERTY(fontFamily, NSString, RCTTextView)
view.font = [RCTFont updateFont:view.font withFamily:json ?: defaultView.font.familyName]; view.font = [RCTFont updateFont:view.font withFamily:json ?: defaultView.font.familyName];
} }
RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger) RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
#if !TARGET_OS_TV
RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, textView.dataDetectorTypes, UIDataDetectorTypes) RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, textView.dataDetectorTypes, UIDataDetectorTypes)
#endif
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(RCTShadowView *)shadowView
{ {

View File

@ -12,7 +12,7 @@
'use strict'; 'use strict';
var Platform = { const Platform = {
OS: 'android', OS: 'android',
get Version() { get Version() {
const AndroidConstants = require('NativeModules').AndroidConstants; const AndroidConstants = require('NativeModules').AndroidConstants;

View File

@ -12,10 +12,15 @@
'use strict'; 'use strict';
var Platform = { const Platform = {
OS: 'ios', OS: 'ios',
get Version() { get Version() {
return require('NativeModules').IOSConstants.osVersion; const constants = require('NativeModules').IOSConstants;
return constants ? constants.osVersion : '';
},
get isTVOS() {
const constants = require('NativeModules').IOSConstants;
return constants ? (constants.interfaceIdiom === 'tv') : false;
}, },
select: (obj: Object) => obj.ios, select: (obj: Object) => obj.ios,
}; };

View File

@ -38,6 +38,7 @@ RCT_EXPORT_MODULE(IOSConstants)
return @{ return @{
@"forceTouchAvailable": @(RCTForceTouchAvailable()), @"forceTouchAvailable": @(RCTForceTouchAvailable()),
@"osVersion": [device systemVersion], @"osVersion": [device systemVersion],
@"systemName": [device systemName],
@"interfaceIdiom": interfaceIdiom([device userInterfaceIdiom]), @"interfaceIdiom": interfaceIdiom([device userInterfaceIdiom]),
}; };
} }

View File

@ -27,6 +27,11 @@
#import "UIView+React.h" #import "UIView+React.h"
#import "RCTProfile.h" #import "RCTProfile.h"
#if TARGET_OS_TV
#import "RCTTVRemoteHandler.h"
#import "RCTTVNavigationEventEmitter.h"
#endif
NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification"; NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotification";
@interface RCTUIManager (RCTRootView) @interface RCTUIManager (RCTRootView)
@ -92,6 +97,13 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat
name:RCTContentDidAppearNotification name:RCTContentDidAppearNotification
object:self]; object:self];
#if TARGET_OS_TV
self.tvRemoteHandler = [RCTTVRemoteHandler new];
for (UIGestureRecognizer *gr in self.tvRemoteHandler.tvRemoteGestureRecognizers) {
[self addGestureRecognizer:gr];
}
#endif
if (!_bridge.loading) { if (!_bridge.loading) {
[self bundleFinishedLoading:[_bridge batchedBridge]]; [self bundleFinishedLoading:[_bridge batchedBridge]];
} }
@ -119,6 +131,16 @@ NSString *const RCTContentDidAppearNotification = @"RCTContentDidAppearNotificat
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame) RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder) RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
#if TARGET_OS_TV
- (UIView *)preferredFocusedView
{
if (self.reactPreferredFocusedView) {
return self.reactPreferredFocusedView;
}
return [super preferredFocusedView];
}
#endif
- (void)setBackgroundColor:(UIColor *)backgroundColor - (void)setBackgroundColor:(UIColor *)backgroundColor
{ {
super.backgroundColor = backgroundColor; super.backgroundColor = backgroundColor;

View File

@ -9,6 +9,8 @@
#import <React/RCTRootView.h> #import <React/RCTRootView.h>
@class RCTTVRemoteHandler;
/** /**
* The interface provides a set of functions that allow other internal framework * The interface provides a set of functions that allow other internal framework
* classes to change the RCTRootViews's internal state. * classes to change the RCTRootViews's internal state.
@ -20,4 +22,12 @@
*/ */
@property (readwrite, nonatomic, assign) CGSize intrinsicSize; @property (readwrite, nonatomic, assign) CGSize intrinsicSize;
/**
* TV remote gesture recognizers
*/
#if TARGET_OS_TV
@property (nonatomic, strong) RCTTVRemoteHandler *tvRemoteHandler;
@property (nonatomic, strong) UIView *reactPreferredFocusedView;
#endif
@end @end

View File

@ -0,0 +1,16 @@
/**
* 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.
*/
#import <UIKit/UIKit.h>
@interface RCTTVRemoteHandler : NSObject
@property (nonatomic, copy, readonly) NSArray *tvRemoteGestureRecognizers;
@end

View File

@ -0,0 +1,153 @@
/**
* 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.
*/
#import "RCTTVRemoteHandler.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTRootView.h"
#import "RCTTVNavigationEventEmitter.h"
#import "RCTUIManager.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "UIView+React.h"
@implementation RCTTVRemoteHandler {
NSMutableArray<UIGestureRecognizer *> *_tvRemoteGestureRecognizers;
}
- (instancetype)init
{
if ((self = [super init])) {
_tvRemoteGestureRecognizers = [NSMutableArray array];
// Recognizers for Apple TV remote buttons
// Play/Pause
[self addTapGestureRecognizerWithSelector:@selector(playPausePressed:)
pressType:UIPressTypePlayPause];
// Menu
[self addTapGestureRecognizerWithSelector:@selector(menuPressed:)
pressType:UIPressTypeMenu];
// Select
[self addTapGestureRecognizerWithSelector:@selector(selectPressed:)
pressType:UIPressTypeSelect];
// Up
[self addTapGestureRecognizerWithSelector:@selector(swipedUp:)
pressType:UIPressTypeUpArrow];
// Down
[self addTapGestureRecognizerWithSelector:@selector(swipedDown:)
pressType:UIPressTypeDownArrow];
// Left
[self addTapGestureRecognizerWithSelector:@selector(swipedLeft:)
pressType:UIPressTypeLeftArrow];
// Right
[self addTapGestureRecognizerWithSelector:@selector(swipedRight:)
pressType:UIPressTypeRightArrow];
// Recognizers for Apple TV remote trackpad swipes
// Up
[self addSwipeGestureRecognizerWithSelector:@selector(swipedUp:)
direction:UISwipeGestureRecognizerDirectionUp];
// Down
[self addSwipeGestureRecognizerWithSelector:@selector(swipedDown:)
direction:UISwipeGestureRecognizerDirectionDown];
// Left
[self addSwipeGestureRecognizerWithSelector:@selector(swipedLeft:)
direction:UISwipeGestureRecognizerDirectionLeft];
// Right
[self addSwipeGestureRecognizerWithSelector:@selector(swipedRight:)
direction:UISwipeGestureRecognizerDirectionRight];
}
return self;
}
- (void)playPausePressed:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"playPause" toView:r.view];
}
- (void)menuPressed:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"menu" toView:r.view];
}
- (void)selectPressed:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"select" toView:r.view];
}
- (void)longPress:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"longPress" toView:r.view];
}
- (void)swipedUp:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"up" toView:r.view];
}
- (void)swipedDown:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"down" toView:r.view];
}
- (void)swipedLeft:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"left" toView:r.view];
}
- (void)swipedRight:(UIGestureRecognizer *)r
{
[self sendAppleTVEvent:@"right" toView:r.view];
}
#pragma mark -
- (void)addTapGestureRecognizerWithSelector:(nonnull SEL)selector pressType:(UIPressType)pressType
{
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:selector];
recognizer.allowedPressTypes = @[@(pressType)];
[_tvRemoteGestureRecognizers addObject:recognizer];
}
- (void)addSwipeGestureRecognizerWithSelector:(nonnull SEL)selector direction:(UISwipeGestureRecognizerDirection)direction
{
UISwipeGestureRecognizer *recognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:selector];
recognizer.direction = direction;
[_tvRemoteGestureRecognizers addObject:recognizer];
}
- (void)sendAppleTVEvent:(NSString *)eventType toView:(UIView *)v
{
[[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
object:@{@"eventType":eventType}];
}
@end

View File

@ -0,0 +1,16 @@
/**
* 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.
*/
#import "RCTEventEmitter.h"
RCT_EXTERN NSString *const RCTTVNavigationEventNotification;
@interface RCTTVNavigationEventEmitter : RCTEventEmitter
@end

View File

@ -0,0 +1,47 @@
/**
* 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.
*/
#import "RCTTVNavigationEventEmitter.h"
NSString *const RCTTVNavigationEventNotification = @"RCTTVNavigationEventNotification";
static NSString *const TVNavigationEventName = @"onTVNavEvent";
@implementation RCTTVNavigationEventEmitter
RCT_EXPORT_MODULE()
- (instancetype)init
{
if (self = [super init]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleTVNavigationEventNotification:)
name:RCTTVNavigationEventNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSArray<NSString *> *)supportedEvents
{
return @[TVNavigationEventName];
}
- (void)handleTVNavigationEventNotification:(NSNotification *)notif
{
[self sendEventWithName:TVNavigationEventName body:notif.object];
}
@end

View File

@ -163,6 +163,7 @@
2D3B5EF11D9B09E700451313 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; }; 2D3B5EF11D9B09E700451313 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; };
2D74EAFA1DAE9590003B751B /* RCTMultipartDataTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 006FC4131D9B20820057AAAD /* RCTMultipartDataTask.m */; }; 2D74EAFA1DAE9590003B751B /* RCTMultipartDataTask.m in Sources */ = {isa = PBXBuildFile; fileRef = 006FC4131D9B20820057AAAD /* RCTMultipartDataTask.m */; };
2D8C2E331DA40441000EE098 /* RCTMultipartStreamReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */; }; 2D8C2E331DA40441000EE098 /* RCTMultipartStreamReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 001BFCCF1D8381DE008E587E /* RCTMultipartStreamReader.m */; };
2D9F8B9B1DE398DB00A16144 /* RCTPlatform.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7749431DC1065C007EC8D8 /* RCTPlatform.m */; };
2DD0EFE11DA84F2800B0C975 /* RCTStatusBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */; }; 2DD0EFE11DA84F2800B0C975 /* RCTStatusBarManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */; };
352DCFF01D19F4C20056D623 /* RCTI18nUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DCFEF1D19F4C20056D623 /* RCTI18nUtil.m */; }; 352DCFF01D19F4C20056D623 /* RCTI18nUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 352DCFEF1D19F4C20056D623 /* RCTI18nUtil.m */; };
369123E11DDC75850095B341 /* JSCSamplingProfiler.m in Sources */ = {isa = PBXBuildFile; fileRef = 369123E01DDC75850095B341 /* JSCSamplingProfiler.m */; }; 369123E11DDC75850095B341 /* JSCSamplingProfiler.m in Sources */ = {isa = PBXBuildFile; fileRef = 369123E01DDC75850095B341 /* JSCSamplingProfiler.m */; };
@ -420,6 +421,15 @@
3D3CD9441DE5FC6500167DC4 /* libjschelpers.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D3CD9181DE5FBD800167DC4 /* libjschelpers.a */; }; 3D3CD9441DE5FC6500167DC4 /* libjschelpers.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D3CD9181DE5FBD800167DC4 /* libjschelpers.a */; };
3D3CD9451DE5FC7100167DC4 /* JSBundleType.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D3CD8F51DE5FB2300167DC4 /* JSBundleType.h */; }; 3D3CD9451DE5FC7100167DC4 /* JSBundleType.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D3CD8F51DE5FB2300167DC4 /* JSBundleType.h */; };
3D3CD9471DE5FC7800167DC4 /* oss-compat-util.h in Headers */ = {isa = PBXBuildFile; fileRef = AC70D2EE1DE48AC5002E6351 /* oss-compat-util.h */; }; 3D3CD9471DE5FC7800167DC4 /* oss-compat-util.h in Headers */ = {isa = PBXBuildFile; fileRef = AC70D2EE1DE48AC5002E6351 /* oss-compat-util.h */; };
3D5AC7131E0056C4000F9153 /* RCTTVView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5AC70F1E0056BC000F9153 /* RCTTVView.h */; };
3D5AC7141E0056C7000F9153 /* RCTTVView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D5AC7101E0056BC000F9153 /* RCTTVView.m */; };
3D5AC7191E0056E0000F9153 /* RCTTVNavigationEventEmitter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5AC7151E0056D9000F9153 /* RCTTVNavigationEventEmitter.h */; };
3D5AC71A1E0056E0000F9153 /* RCTTVNavigationEventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D5AC7161E0056D9000F9153 /* RCTTVNavigationEventEmitter.m */; };
3D5AC71B1E005723000F9153 /* RCTReloadCommand.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = A2440AA01DF8D854006E7BFC /* RCTReloadCommand.h */; };
3D5AC71C1E005723000F9153 /* RCTTVNavigationEventEmitter.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3D5AC7151E0056D9000F9153 /* RCTTVNavigationEventEmitter.h */; };
3D5AC71D1E00572F000F9153 /* RCTTVView.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 3D5AC70F1E0056BC000F9153 /* RCTTVView.h */; };
3D5AC7221E005763000F9153 /* RCTTVRemoteHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D5AC71E1E005750000F9153 /* RCTTVRemoteHandler.h */; };
3D5AC7231E005766000F9153 /* RCTTVRemoteHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D5AC71F1E005750000F9153 /* RCTTVRemoteHandler.m */; };
3D7749441DC1065C007EC8D8 /* RCTPlatform.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7749431DC1065C007EC8D8 /* RCTPlatform.m */; }; 3D7749441DC1065C007EC8D8 /* RCTPlatform.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D7749431DC1065C007EC8D8 /* RCTPlatform.m */; };
3D7A27E21DE325B7002E3F95 /* RCTJSCErrorHandling.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D7A27E11DE325B7002E3F95 /* RCTJSCErrorHandling.mm */; }; 3D7A27E21DE325B7002E3F95 /* RCTJSCErrorHandling.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D7A27E11DE325B7002E3F95 /* RCTJSCErrorHandling.mm */; };
3D7A27E31DE325DA002E3F95 /* RCTJSCErrorHandling.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D7A27E11DE325B7002E3F95 /* RCTJSCErrorHandling.mm */; }; 3D7A27E31DE325DA002E3F95 /* RCTJSCErrorHandling.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D7A27E11DE325B7002E3F95 /* RCTJSCErrorHandling.mm */; };
@ -775,6 +785,9 @@
dstPath = include/React; dstPath = include/React;
dstSubfolderSpec = 16; dstSubfolderSpec = 16;
files = ( files = (
3D5AC71D1E00572F000F9153 /* RCTTVView.h in Copy Headers */,
3D5AC71B1E005723000F9153 /* RCTReloadCommand.h in Copy Headers */,
3D5AC71C1E005723000F9153 /* RCTTVNavigationEventEmitter.h in Copy Headers */,
3D302FA01DF8290600D6DDAE /* RCTImageLoader.h in Copy Headers */, 3D302FA01DF8290600D6DDAE /* RCTImageLoader.h in Copy Headers */,
3D302FA11DF8290600D6DDAE /* RCTImageStoreManager.h in Copy Headers */, 3D302FA11DF8290600D6DDAE /* RCTImageStoreManager.h in Copy Headers */,
3D302FA21DF8290600D6DDAE /* RCTResizeMode.h in Copy Headers */, 3D302FA21DF8290600D6DDAE /* RCTResizeMode.h in Copy Headers */,
@ -1283,6 +1296,12 @@
3D3CD9181DE5FBD800167DC4 /* libjschelpers.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libjschelpers.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3D3CD9181DE5FBD800167DC4 /* libjschelpers.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libjschelpers.a; sourceTree = BUILT_PRODUCTS_DIR; };
3D3CD9251DE5FBEC00167DC4 /* libcxxreact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libcxxreact.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3D3CD9251DE5FBEC00167DC4 /* libcxxreact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libcxxreact.a; sourceTree = BUILT_PRODUCTS_DIR; };
3D3CD9321DE5FBEE00167DC4 /* libcxxreact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libcxxreact.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3D3CD9321DE5FBEE00167DC4 /* libcxxreact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libcxxreact.a; sourceTree = BUILT_PRODUCTS_DIR; };
3D5AC70F1E0056BC000F9153 /* RCTTVView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTVView.h; sourceTree = "<group>"; };
3D5AC7101E0056BC000F9153 /* RCTTVView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTVView.m; sourceTree = "<group>"; };
3D5AC7151E0056D9000F9153 /* RCTTVNavigationEventEmitter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTVNavigationEventEmitter.h; sourceTree = "<group>"; };
3D5AC7161E0056D9000F9153 /* RCTTVNavigationEventEmitter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTVNavigationEventEmitter.m; sourceTree = "<group>"; };
3D5AC71E1E005750000F9153 /* RCTTVRemoteHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTVRemoteHandler.h; sourceTree = "<group>"; };
3D5AC71F1E005750000F9153 /* RCTTVRemoteHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTVRemoteHandler.m; sourceTree = "<group>"; };
3D7749421DC1065C007EC8D8 /* RCTPlatform.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPlatform.h; sourceTree = "<group>"; }; 3D7749421DC1065C007EC8D8 /* RCTPlatform.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPlatform.h; sourceTree = "<group>"; };
3D7749431DC1065C007EC8D8 /* RCTPlatform.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPlatform.m; sourceTree = "<group>"; }; 3D7749431DC1065C007EC8D8 /* RCTPlatform.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPlatform.m; sourceTree = "<group>"; };
3D7A27DC1DE32541002E3F95 /* JavaScriptCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JavaScriptCore.h; sourceTree = "<group>"; }; 3D7A27DC1DE32541002E3F95 /* JavaScriptCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JavaScriptCore.h; sourceTree = "<group>"; };
@ -1433,6 +1452,8 @@
13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */, 13723B4F1A82FD3C00F88898 /* RCTStatusBarManager.m */,
13B07FED1A69327A00A75B9A /* RCTTiming.h */, 13B07FED1A69327A00A75B9A /* RCTTiming.h */,
13B07FEE1A69327A00A75B9A /* RCTTiming.m */, 13B07FEE1A69327A00A75B9A /* RCTTiming.m */,
3D5AC7151E0056D9000F9153 /* RCTTVNavigationEventEmitter.h */,
3D5AC7161E0056D9000F9153 /* RCTTVNavigationEventEmitter.m */,
13E067481A70F434002CDEE1 /* RCTUIManager.h */, 13E067481A70F434002CDEE1 /* RCTUIManager.h */,
13E067491A70F434002CDEE1 /* RCTUIManager.m */, 13E067491A70F434002CDEE1 /* RCTUIManager.m */,
); );
@ -1527,6 +1548,8 @@
137327E51AA5CF210034F82E /* RCTTabBarManager.h */, 137327E51AA5CF210034F82E /* RCTTabBarManager.h */,
137327E61AA5CF210034F82E /* RCTTabBarManager.m */, 137327E61AA5CF210034F82E /* RCTTabBarManager.m */,
E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */, E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */,
3D5AC70F1E0056BC000F9153 /* RCTTVView.h */,
3D5AC7101E0056BC000F9153 /* RCTTVView.m */,
13E0674F1A70F44B002CDEE1 /* RCTView.h */, 13E0674F1A70F44B002CDEE1 /* RCTView.h */,
13E067501A70F44B002CDEE1 /* RCTView.m */, 13E067501A70F44B002CDEE1 /* RCTView.m */,
13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */, 13442BF41AA90E0B0037E5B0 /* RCTViewControllerProtocol.h */,
@ -1734,6 +1757,8 @@
391E86A21C623EC800009732 /* RCTTouchEvent.m */, 391E86A21C623EC800009732 /* RCTTouchEvent.m */,
83CBBA961A6020BB00E9B192 /* RCTTouchHandler.h */, 83CBBA961A6020BB00E9B192 /* RCTTouchHandler.h */,
83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */, 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */,
3D5AC71E1E005750000F9153 /* RCTTVRemoteHandler.h */,
3D5AC71F1E005750000F9153 /* RCTTVRemoteHandler.m */,
1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */, 1345A83A1B265A0E00583190 /* RCTURLRequestDelegate.h */,
1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */, 1345A83B1B265A0E00583190 /* RCTURLRequestHandler.h */,
83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */,
@ -1781,6 +1806,7 @@
3D302F361DF828F800D6DDAE /* RCTErrorInfo.h in Headers */, 3D302F361DF828F800D6DDAE /* RCTErrorInfo.h in Headers */,
3D302F371DF828F800D6DDAE /* RCTEventDispatcher.h in Headers */, 3D302F371DF828F800D6DDAE /* RCTEventDispatcher.h in Headers */,
3D302F381DF828F800D6DDAE /* RCTFrameUpdate.h in Headers */, 3D302F381DF828F800D6DDAE /* RCTFrameUpdate.h in Headers */,
3D5AC7221E005763000F9153 /* RCTTVRemoteHandler.h in Headers */,
3D302F391DF828F800D6DDAE /* RCTImageSource.h in Headers */, 3D302F391DF828F800D6DDAE /* RCTImageSource.h in Headers */,
3D302F3A1DF828F800D6DDAE /* RCTInvalidating.h in Headers */, 3D302F3A1DF828F800D6DDAE /* RCTInvalidating.h in Headers */,
3D302F3B1DF828F800D6DDAE /* RCTJavaScriptExecutor.h in Headers */, 3D302F3B1DF828F800D6DDAE /* RCTJavaScriptExecutor.h in Headers */,
@ -1854,6 +1880,7 @@
3D302F841DF828F800D6DDAE /* RCTPointerEvents.h in Headers */, 3D302F841DF828F800D6DDAE /* RCTPointerEvents.h in Headers */,
3D302F851DF828F800D6DDAE /* RCTProgressViewManager.h in Headers */, 3D302F851DF828F800D6DDAE /* RCTProgressViewManager.h in Headers */,
3D302F861DF828F800D6DDAE /* RCTRefreshControl.h in Headers */, 3D302F861DF828F800D6DDAE /* RCTRefreshControl.h in Headers */,
3D5AC7131E0056C4000F9153 /* RCTTVView.h in Headers */,
3D302F871DF828F800D6DDAE /* RCTRefreshControlManager.h in Headers */, 3D302F871DF828F800D6DDAE /* RCTRefreshControlManager.h in Headers */,
A2440AA41DF8D865006E7BFC /* RCTReloadCommand.h in Headers */, A2440AA41DF8D865006E7BFC /* RCTReloadCommand.h in Headers */,
3D302F881DF828F800D6DDAE /* RCTRootShadowView.h in Headers */, 3D302F881DF828F800D6DDAE /* RCTRootShadowView.h in Headers */,
@ -1865,6 +1892,7 @@
3D302F8E1DF828F800D6DDAE /* RCTShadowView.h in Headers */, 3D302F8E1DF828F800D6DDAE /* RCTShadowView.h in Headers */,
3D302F8F1DF828F800D6DDAE /* RCTSlider.h in Headers */, 3D302F8F1DF828F800D6DDAE /* RCTSlider.h in Headers */,
3D302F901DF828F800D6DDAE /* RCTSliderManager.h in Headers */, 3D302F901DF828F800D6DDAE /* RCTSliderManager.h in Headers */,
3D5AC7191E0056E0000F9153 /* RCTTVNavigationEventEmitter.h in Headers */,
3D302F911DF828F800D6DDAE /* RCTSwitch.h in Headers */, 3D302F911DF828F800D6DDAE /* RCTSwitch.h in Headers */,
3D302F921DF828F800D6DDAE /* RCTSwitchManager.h in Headers */, 3D302F921DF828F800D6DDAE /* RCTSwitchManager.h in Headers */,
3D302F931DF828F800D6DDAE /* RCTTabBar.h in Headers */, 3D302F931DF828F800D6DDAE /* RCTTabBar.h in Headers */,
@ -2338,6 +2366,7 @@
2D3B5EC91D9B095C00451313 /* RCTBorderDrawing.m in Sources */, 2D3B5EC91D9B095C00451313 /* RCTBorderDrawing.m in Sources */,
2D3B5ED31D9B097B00451313 /* RCTMapOverlay.m in Sources */, 2D3B5ED31D9B097B00451313 /* RCTMapOverlay.m in Sources */,
2D3B5E991D9B089A00451313 /* RCTDisplayLink.m in Sources */, 2D3B5E991D9B089A00451313 /* RCTDisplayLink.m in Sources */,
2D9F8B9B1DE398DB00A16144 /* RCTPlatform.m in Sources */,
2D3B5EBF1D9B093300451313 /* RCTJSCProfiler.m in Sources */, 2D3B5EBF1D9B093300451313 /* RCTJSCProfiler.m in Sources */,
2D3B5EA11D9B08B600451313 /* RCTModuleData.mm in Sources */, 2D3B5EA11D9B08B600451313 /* RCTModuleData.mm in Sources */,
2D3B5EEA1D9B09CD00451313 /* RCTTabBar.m in Sources */, 2D3B5EEA1D9B09CD00451313 /* RCTTabBar.m in Sources */,
@ -2393,15 +2422,18 @@
2D3B5EC81D9B095800451313 /* RCTActivityIndicatorViewManager.m in Sources */, 2D3B5EC81D9B095800451313 /* RCTActivityIndicatorViewManager.m in Sources */,
3DCD185D1DF978E7007FE5A1 /* RCTReloadCommand.m in Sources */, 3DCD185D1DF978E7007FE5A1 /* RCTReloadCommand.m in Sources */,
2D3B5EC61D9B095000451313 /* RCTProfileTrampoline-x86_64.S in Sources */, 2D3B5EC61D9B095000451313 /* RCTProfileTrampoline-x86_64.S in Sources */,
3D5AC71A1E0056E0000F9153 /* RCTTVNavigationEventEmitter.m in Sources */,
3D7A27E31DE325DA002E3F95 /* RCTJSCErrorHandling.mm in Sources */, 3D7A27E31DE325DA002E3F95 /* RCTJSCErrorHandling.mm in Sources */,
2D3B5ED01D9B097200451313 /* RCTMap.m in Sources */, 2D3B5ED01D9B097200451313 /* RCTMap.m in Sources */,
2D3B5EA61D9B08CA00451313 /* RCTTouchEvent.m in Sources */, 2D3B5EA61D9B08CA00451313 /* RCTTouchEvent.m in Sources */,
2D8C2E331DA40441000EE098 /* RCTMultipartStreamReader.m in Sources */, 2D8C2E331DA40441000EE098 /* RCTMultipartStreamReader.m in Sources */,
2D3B5EF01D9B09E300451313 /* RCTWrapperViewController.m in Sources */, 2D3B5EF01D9B09E300451313 /* RCTWrapperViewController.m in Sources */,
3D5AC7141E0056C7000F9153 /* RCTTVView.m in Sources */,
2D3B5EEC1D9B09D400451313 /* RCTTabBarItemManager.m in Sources */, 2D3B5EEC1D9B09D400451313 /* RCTTabBarItemManager.m in Sources */,
2D3B5EB01D9B08FE00451313 /* RCTAlertManager.m in Sources */, 2D3B5EB01D9B08FE00451313 /* RCTAlertManager.m in Sources */,
2D3B5E9C1D9B08A300451313 /* RCTImageSource.m in Sources */, 2D3B5E9C1D9B08A300451313 /* RCTImageSource.m in Sources */,
3DDEC1521DDCE0CA0020BBDF /* JSCSamplingProfiler.m in Sources */, 3DDEC1521DDCE0CA0020BBDF /* JSCSamplingProfiler.m in Sources */,
3D5AC7231E005766000F9153 /* RCTTVRemoteHandler.m in Sources */,
2D3B5EC31D9B094800451313 /* RCTProfileTrampoline-arm.S in Sources */, 2D3B5EC31D9B094800451313 /* RCTProfileTrampoline-arm.S in Sources */,
2D3B5ED91D9B098E00451313 /* RCTNavItem.m in Sources */, 2D3B5ED91D9B098E00451313 /* RCTNavItem.m in Sources */,
2D74EAFA1DAE9590003B751B /* RCTMultipartDataTask.m in Sources */, 2D74EAFA1DAE9590003B751B /* RCTMultipartDataTask.m in Sources */,

33
React/Views/RCTTVView.h Normal file
View File

@ -0,0 +1,33 @@
/**
* 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.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
// A RCTView with additional properties and methods for user interaction using the Apple TV focus engine.
@interface RCTTVView : RCTView
/**
* TV event handlers
*/
@property (nonatomic, assign) BOOL isTVSelectable; // True if this view is TV-focusable
/**
* Properties for Apple TV focus parallax effects
*/
@property (nonatomic, copy) NSDictionary *tvParallaxProperties;
/**
* TV preferred focus
*/
@property (nonatomic, assign) BOOL hasTVPreferredFocus;
@end

188
React/Views/RCTTVView.m Normal file
View File

@ -0,0 +1,188 @@
/**
* 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.
*/
#import "RCTTVView.h"
#import "RCTAutoInsetsProtocol.h"
#import "RCTBorderDrawing.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTEventDispatcher.h"
#import "RCTLog.h"
#import "RCTRootViewInternal.h"
#import "RCTTVNavigationEventEmitter.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "UIView+React.h"
@implementation RCTTVView
{
UITapGestureRecognizer *_selectRecognizer;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.tvParallaxProperties = @{
@"enabled": @YES,
@"shiftDistanceX": @2.0f,
@"shiftDistanceY": @2.0f,
@"tiltAngle": @0.05f,
@"magnification": @1.0f
};
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
- (void)setIsTVSelectable:(BOOL)isTVSelectable {
self->_isTVSelectable = isTVSelectable;
if(isTVSelectable) {
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelect:)];
recognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
_selectRecognizer = recognizer;
[self addGestureRecognizer:_selectRecognizer];
} else {
if(_selectRecognizer) {
[self removeGestureRecognizer:_selectRecognizer];
}
}
}
- (void)handleSelect:(UIGestureRecognizer *)r
{
[[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
object:@{@"eventType":@"select",@"tag":self.reactTag}];
}
- (BOOL)isUserInteractionEnabled
{
return YES;
}
- (BOOL)canBecomeFocused
{
return (self.isTVSelectable);
}
- (void)addParallaxMotionEffects
{
// Size of shift movements
CGFloat const shiftDistanceX = [self.tvParallaxProperties[@"shiftDistanceX"] floatValue];
CGFloat const shiftDistanceY = [self.tvParallaxProperties[@"shiftDistanceY"] floatValue];
// Make horizontal movements shift the centre left and right
UIInterpolatingMotionEffect *xShift = [[UIInterpolatingMotionEffect alloc]
initWithKeyPath:@"center.x"
type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
xShift.minimumRelativeValue = @( shiftDistanceX * -1.0f);
xShift.maximumRelativeValue = @( shiftDistanceX);
// Make vertical movements shift the centre up and down
UIInterpolatingMotionEffect *yShift = [[UIInterpolatingMotionEffect alloc]
initWithKeyPath:@"center.y"
type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];
yShift.minimumRelativeValue = @( shiftDistanceY * -1.0f);
yShift.maximumRelativeValue = @( shiftDistanceY);
// Size of tilt movements
CGFloat const tiltAngle = [self.tvParallaxProperties[@"tiltAngle"] floatValue];
// Now make horizontal movements effect a rotation about the Y axis for side-to-side rotation.
UIInterpolatingMotionEffect *xTilt = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"layer.transform" type:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis];
// CATransform3D value for minimumRelativeValue
CATransform3D transMinimumTiltAboutY = CATransform3DIdentity;
transMinimumTiltAboutY.m34 = 1.0 / 500;
transMinimumTiltAboutY = CATransform3DRotate(transMinimumTiltAboutY, tiltAngle * -1.0, 0, 1, 0);
// CATransform3D value for minimumRelativeValue
CATransform3D transMaximumTiltAboutY = CATransform3DIdentity;
transMaximumTiltAboutY.m34 = 1.0 / 500;
transMaximumTiltAboutY = CATransform3DRotate(transMaximumTiltAboutY, tiltAngle, 0, 1, 0);
// Set the transform property boundaries for the interpolation
xTilt.minimumRelativeValue = [NSValue valueWithCATransform3D: transMinimumTiltAboutY];
xTilt.maximumRelativeValue = [NSValue valueWithCATransform3D: transMaximumTiltAboutY];
// Now make vertical movements effect a rotation about the X axis for up and down rotation.
UIInterpolatingMotionEffect *yTilt = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"layer.transform" type:UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis];
// CATransform3D value for minimumRelativeValue
CATransform3D transMinimumTiltAboutX = CATransform3DIdentity;
transMinimumTiltAboutX.m34 = 1.0 / 500;
transMinimumTiltAboutX = CATransform3DRotate(transMinimumTiltAboutX, tiltAngle * -1.0, 1, 0, 0);
// CATransform3D value for minimumRelativeValue
CATransform3D transMaximumTiltAboutX = CATransform3DIdentity;
transMaximumTiltAboutX.m34 = 1.0 / 500;
transMaximumTiltAboutX = CATransform3DRotate(transMaximumTiltAboutX, tiltAngle, 1, 0, 0);
// Set the transform property boundaries for the interpolation
yTilt.minimumRelativeValue = [NSValue valueWithCATransform3D: transMinimumTiltAboutX];
yTilt.maximumRelativeValue = [NSValue valueWithCATransform3D: transMaximumTiltAboutX];
// Add all of the motion effects to this group
self.motionEffects = @[xShift, yShift, xTilt, yTilt];
float magnification = [self.tvParallaxProperties[@"magnification"] floatValue];
[UIView animateWithDuration:0.2 animations:^{
self.transform = CGAffineTransformMakeScale(magnification, magnification);
}];
}
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
if (context.nextFocusedView == self && self.isTVSelectable ) {
[self becomeFirstResponder];
[coordinator addCoordinatedAnimations:^(void){
if([self.tvParallaxProperties[@"enabled"] boolValue]) {
[self addParallaxMotionEffects];
}
[[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
object:@{@"eventType":@"focus",@"tag":self.reactTag}];
} completion:^(void){}];
} else {
[coordinator addCoordinatedAnimations:^(void){
[[NSNotificationCenter defaultCenter] postNotificationName:RCTTVNavigationEventNotification
object:@{@"eventType":@"blur",@"tag":self.reactTag}];
[UIView animateWithDuration:0.2 animations:^{
self.transform = CGAffineTransformMakeScale(1, 1);
}];
for (UIMotionEffect *effect in [self.motionEffects copy]){
[self removeMotionEffect:effect];
}
} completion:^(void){}];
[self resignFirstResponder];
}
}
- (void)setHasTVPreferredFocus:(BOOL)hasTVPreferredFocus
{
_hasTVPreferredFocus = hasTVPreferredFocus;
if (hasTVPreferredFocus) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIView *rootview = self;
while(![rootview isReactRootView]) {
rootview = [rootview superview];
}
rootview = [rootview superview];
[(RCTRootView *)rootview setReactPreferredFocusedView:self];
[rootview setNeedsFocusUpdate];
[rootview updateFocusIfNeeded];
});
}
}
@end

View File

@ -183,4 +183,22 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
return NO; return NO;
} }
#if TARGET_OS_TV
- (BOOL)isUserInteractionEnabled
{
return YES;
}
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
{
if (context.nextFocusedView == self) {
[self becomeFirstResponder];
} else {
[self resignFirstResponder];
}
}
#endif
@end @end

View File

@ -29,6 +29,7 @@ RCT_EXPORT_VIEW_PROPERTY(selectedIcon, UIImage)
RCT_EXPORT_VIEW_PROPERTY(systemIcon, UITabBarSystemItem) RCT_EXPORT_VIEW_PROPERTY(systemIcon, UITabBarSystemItem)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(badgeColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(badgeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(isTVSelectable, BOOL)
RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem) RCT_CUSTOM_VIEW_PROPERTY(title, NSString, RCTTabBarItem)
{ {
view.barItem.title = json ? [RCTConvert NSString:json] : defaultView.barItem.title; view.barItem.title = json ? [RCTConvert NSString:json] : defaultView.barItem.title;

View File

@ -20,6 +20,10 @@
#import "RCTView.h" #import "RCTView.h"
#import "UIView+React.h" #import "UIView+React.h"
#if TARGET_OS_TV
#import "RCTTVView.h"
#endif
@implementation RCTConvert(UIAccessibilityTraits) @implementation RCTConvert(UIAccessibilityTraits)
RCT_MULTI_ENUM_CONVERTER(UIAccessibilityTraits, (@{ RCT_MULTI_ENUM_CONVERTER(UIAccessibilityTraits, (@{
@ -57,7 +61,11 @@ RCT_EXPORT_MODULE()
- (UIView *)view - (UIView *)view
{ {
#if TARGET_OS_TV
return [RCTTVView new];
#else
return [RCTView new]; return [RCTView new];
#endif
} }
- (RCTShadowView *)shadowView - (RCTShadowView *)shadowView
@ -98,6 +106,13 @@ RCT_EXPORT_MODULE()
#pragma mark - View properties #pragma mark - View properties
#if TARGET_OS_TV
// Apple TV properties
RCT_EXPORT_VIEW_PROPERTY(isTVSelectable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(hasTVPreferredFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tvParallaxProperties, NSDictionary)
#endif
RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel, NSString) RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel, NSString)
RCT_EXPORT_VIEW_PROPERTY(accessibilityTraits, UIAccessibilityTraits) RCT_EXPORT_VIEW_PROPERTY(accessibilityTraits, UIAccessibilityTraits)
RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor)

View File

@ -11,7 +11,7 @@ XCODE_PROJECT="Examples/UIExplorer/UIExplorer.xcodeproj"
XCODE_SCHEME="UIExplorer-tvOS" XCODE_SCHEME="UIExplorer-tvOS"
XCODE_SDK="appletvsimulator" XCODE_SDK="appletvsimulator"
if [ -z ${XCODE_DESTINATION+x} ]; then if [ -z ${XCODE_DESTINATION+x} ]; then
XCODE_DESTINATION="platform=tvOS Simulator,name=Apple TV 1080p,OS=9.2" XCODE_DESTINATION="platform=tvOS Simulator,name=Apple TV 1080p,OS=10.0"
fi fi
. ./scripts/objc-test.sh . ./scripts/objc-test.sh