Improved 3D touch implementation, and added example

Summary:
public
This diff improves the implementation of 3D touch by adding a `forceTouchAvailable` constant to View that can be used to check if the feature is supported.

I've also added an example of how you can use the `force` property of the touch event to measure touch pressure in React Native.

Reviewed By: vjeux

Differential Revision: D2864926

fb-gh-sync-id: 754c54989212ce4e4863716ceaba59673f0bb29d
This commit is contained in:
Nick Lockwood 2016-01-27 09:04:14 -08:00 committed by facebook-github-bot-9
parent 21a4c6e853
commit f685878938
7 changed files with 144 additions and 60 deletions

View File

@ -23,6 +23,7 @@ var {
Text, Text,
TouchableHighlight, TouchableHighlight,
TouchableOpacity, TouchableOpacity,
UIManager,
View, View,
} = React; } = React;
@ -85,6 +86,13 @@ exports.examples = [
render: function(): ReactElement { render: function(): ReactElement {
return <TouchableDelayEvents />; return <TouchableDelayEvents />;
}, },
}, {
title: '3D Touch / Force Touch',
description: 'iPhone 6s and 6s plus support 3D touch, which adds a force property to touches',
render: function(): ReactElement {
return <ForceTouchExample />;
},
platform: 'ios',
}]; }];
var TextOnPressBox = React.createClass({ var TextOnPressBox = React.createClass({
@ -133,18 +141,18 @@ var TouchableFeedbackEvents = React.createClass({
return ( return (
<View testID="touchable_feedback_events"> <View testID="touchable_feedback_events">
<View style={[styles.row, {justifyContent: 'center'}]}> <View style={[styles.row, {justifyContent: 'center'}]}>
<TouchableOpacity <TouchableOpacity
style={styles.wrapper} style={styles.wrapper}
testID="touchable_feedback_events_button" testID="touchable_feedback_events_button"
onPress={() => this._appendEvent('press')} onPress={() => this._appendEvent('press')}
onPressIn={() => this._appendEvent('pressIn')} onPressIn={() => this._appendEvent('pressIn')}
onPressOut={() => this._appendEvent('pressOut')} onPressOut={() => this._appendEvent('pressOut')}
onLongPress={() => this._appendEvent('longPress')}> onLongPress={() => this._appendEvent('longPress')}>
<Text style={styles.button}> <Text style={styles.button}>
Press Me Press Me
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View testID="touchable_feedback_events_console" style={styles.eventLogBox}> <View testID="touchable_feedback_events_console" style={styles.eventLogBox}>
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)} {this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
</View> </View>
@ -169,21 +177,21 @@ var TouchableDelayEvents = React.createClass({
return ( return (
<View testID="touchable_delay_events"> <View testID="touchable_delay_events">
<View style={[styles.row, {justifyContent: 'center'}]}> <View style={[styles.row, {justifyContent: 'center'}]}>
<TouchableOpacity <TouchableOpacity
style={styles.wrapper} style={styles.wrapper}
testID="touchable_delay_events_button" testID="touchable_delay_events_button"
onPress={() => this._appendEvent('press')} onPress={() => this._appendEvent('press')}
delayPressIn={400} delayPressIn={400}
onPressIn={() => this._appendEvent('pressIn - 400ms delay')} onPressIn={() => this._appendEvent('pressIn - 400ms delay')}
delayPressOut={1000} delayPressOut={1000}
onPressOut={() => this._appendEvent('pressOut - 1000ms delay')} onPressOut={() => this._appendEvent('pressOut - 1000ms delay')}
delayLongPress={800} delayLongPress={800}
onLongPress={() => this._appendEvent('longPress - 800ms delay')}> onLongPress={() => this._appendEvent('longPress - 800ms delay')}>
<Text style={styles.button}> <Text style={styles.button}>
Press Me Press Me
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.eventLogBox} testID="touchable_delay_events_console"> <View style={styles.eventLogBox} testID="touchable_delay_events_console">
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)} {this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
</View> </View>
@ -198,6 +206,40 @@ var TouchableDelayEvents = React.createClass({
}, },
}); });
var ForceTouchExample = React.createClass({
getInitialState: function() {
return {
force: 0,
};
},
_renderConsoleText: function() {
return View.forceTouchAvailable ?
'Force: ' + this.state.force.toFixed(3) :
'3D Touch is not available on this device';
},
render: function() {
return (
<View testID="touchable_3dtouch_event">
<View style={styles.forceTouchBox} testID="touchable_3dtouch_output">
<Text>{this._renderConsoleText()}</Text>
</View>
<View style={[styles.row, {justifyContent: 'center'}]}>
<View
style={styles.wrapper}
testID="touchable_3dtouch_button"
onStartShouldSetResponder={() => true}
onResponderMove={(event) => this.setState({force: event.nativeEvent.force})}
onResponderRelease={(event) => this.setState({force: 0})}>
<Text style={styles.button}>
Press Me
</Text>
</View>
</View>
</View>
);
},
});
var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'}; var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'};
var styles = StyleSheet.create({ var styles = StyleSheet.create({
@ -241,6 +283,14 @@ var styles = StyleSheet.create({
borderColor: '#f0f0f0', borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9', backgroundColor: '#f9f9f9',
}, },
forceTouchBox: {
padding: 10,
margin: 10,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9',
alignItems: 'center',
},
textBlock: { textBlock: {
fontWeight: '500', fontWeight: '500',
color: 'blue', color: 'blue',

View File

@ -49,24 +49,24 @@ var queryLayoutByID = require('queryLayoutByID');
* *
* - Choose the rendered component who's touches should start the interactive * - Choose the rendered component who's touches should start the interactive
* sequence. On that rendered node, forward all `Touchable` responder * sequence. On that rendered node, forward all `Touchable` responder
* handlers. You can choose any rendered node you like. Choose a node who's * handlers. You can choose any rendered node you like. Choose a node whose
* hit target you'd like to instigate the interaction sequence: * hit target you'd like to instigate the interaction sequence:
* *
* // In render function: * // In render function:
* return ( * return (
* <div * <View
* onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} * onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
* onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest} * onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
* onResponderGrant={this.touchableHandleResponderGrant} * onResponderGrant={this.touchableHandleResponderGrant}
* onResponderMove={this.touchableHandleResponderMove} * onResponderMove={this.touchableHandleResponderMove}
* onResponderRelease={this.touchableHandleResponderRelease} * onResponderRelease={this.touchableHandleResponderRelease}
* onResponderTerminate={this.touchableHandleResponderTerminate}> * onResponderTerminate={this.touchableHandleResponderTerminate}>
* <div> * <View>
* Even though the hit detection/interactions are triggered by the * Even though the hit detection/interactions are triggered by the
* wrapping (typically larger) node, we usually end up implementing * wrapping (typically larger) node, we usually end up implementing
* custom logic that highlights this inner one. * custom logic that highlights this inner one.
* </div> * </View>
* </div> * </View>
* ); * );
* *
* - You may set up your own handlers for each of these events, so long as you * - You may set up your own handlers for each of these events, so long as you

View File

@ -11,20 +11,20 @@
*/ */
'use strict'; 'use strict';
var NativeMethodsMixin = require('NativeMethodsMixin'); const NativeMethodsMixin = require('NativeMethodsMixin');
var PropTypes = require('ReactPropTypes'); const PropTypes = require('ReactPropTypes');
var React = require('React'); const React = require('React');
var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); const ReactNativeStyleAttributes = require('ReactNativeStyleAttributes');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var StyleSheetPropType = require('StyleSheetPropType'); const StyleSheetPropType = require('StyleSheetPropType');
var UIManager = require('UIManager'); const UIManager = require('UIManager');
var ViewStylePropTypes = require('ViewStylePropTypes'); const ViewStylePropTypes = require('ViewStylePropTypes');
var requireNativeComponent = require('requireNativeComponent'); const requireNativeComponent = require('requireNativeComponent');
var stylePropType = StyleSheetPropType(ViewStylePropTypes); const stylePropType = StyleSheetPropType(ViewStylePropTypes);
var AccessibilityTraits = [ const AccessibilityTraits = [
'none', 'none',
'button', 'button',
'link', 'link',
@ -44,13 +44,26 @@ var AccessibilityTraits = [
'pageTurn', 'pageTurn',
]; ];
var AccessibilityComponentType = [ const AccessibilityComponentType = [
'none', 'none',
'button', 'button',
'radiobutton_checked', 'radiobutton_checked',
'radiobutton_unchecked', 'radiobutton_unchecked',
]; ];
const forceTouchAvailable = (UIManager.RCTView.Constants &&
UIManager.RCTView.Constants.forceTouchAvailable) || false;
const statics = {
AccessibilityTraits,
AccessibilityComponentType,
/**
* Is 3D Touch / Force Touch available (i.e. will touch events include `force`)
* @platform ios
*/
forceTouchAvailable,
};
/** /**
* The most fundamental component for building UI, `View` is a * The most fundamental component for building UI, `View` is a
* container that supports layout with flexbox, style, some touch handling, and * container that supports layout with flexbox, style, some touch handling, and
@ -71,7 +84,7 @@ var AccessibilityComponentType = [
* `View`s are designed to be used with `StyleSheet`s for clarity and * `View`s are designed to be used with `StyleSheet`s for clarity and
* performance, although inline styles are also supported. * performance, although inline styles are also supported.
*/ */
var View = React.createClass({ const View = React.createClass({
mixins: [NativeMethodsMixin], mixins: [NativeMethodsMixin],
/** /**
@ -84,8 +97,7 @@ var View = React.createClass({
}, },
statics: { statics: {
AccessibilityTraits, ...statics,
AccessibilityComponentType,
}, },
propTypes: { propTypes: {
@ -320,16 +332,16 @@ var View = React.createClass({
}, },
}); });
var RCTView = requireNativeComponent('RCTView', View, { const RCTView = requireNativeComponent('RCTView', View, {
nativeOnly: { nativeOnly: {
nativeBackgroundAndroid: true, nativeBackgroundAndroid: true,
} }
}); });
if (__DEV__) { if (__DEV__) {
var viewConfig = UIManager.viewConfigs && UIManager.viewConfigs.RCTView || {}; const viewConfig = UIManager.viewConfigs && UIManager.viewConfigs.RCTView || {};
for (var prop in viewConfig.nativeProps) { for (const prop in viewConfig.nativeProps) {
var viewAny: any = View; // Appease flow const viewAny: any = View; // Appease flow
if (!viewAny.propTypes[prop] && !ReactNativeStyleAttributes[prop]) { if (!viewAny.propTypes[prop] && !ReactNativeStyleAttributes[prop]) {
throw new Error( throw new Error(
'View is missing propType for native prop `' + prop + '`' 'View is missing propType for native prop `' + prop + '`'
@ -338,9 +350,11 @@ if (__DEV__) {
} }
} }
var ViewToExport = RCTView; let ViewToExport = RCTView;
if (__DEV__) { if (__DEV__) {
ViewToExport = View; ViewToExport = View;
} else {
Object.assign(RCTView, statics);
} }
module.exports = ViewToExport; module.exports = ViewToExport;

View File

@ -108,11 +108,9 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
} }
// Create touch // Create touch
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:11]; NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:RCTMaxTouches];
reactTouch[@"target"] = reactTag; reactTouch[@"target"] = reactTag;
reactTouch[@"identifier"] = @(touchID); reactTouch[@"identifier"] = @(touchID);
reactTouch[@"touches"] = (id)kCFNull; // We hijack this touchObj to serve both as an event
reactTouch[@"changedTouches"] = (id)kCFNull; // and as a Touch object, so making this JIT friendly.
// Add to arrays // Add to arrays
[_touchViews addObject:targetView]; [_touchViews addObject:targetView];
@ -151,10 +149,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
reactTouch[@"locationY"] = @(touchViewLocation.y); reactTouch[@"locationY"] = @(touchViewLocation.y);
reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS
if ([nativeTouch.view respondsToSelector:@selector(traitCollection)] && // TODO: force for a 'normal' touch is usually 1.0;
[nativeTouch.view.traitCollection respondsToSelector:@selector(forceTouchCapability)]) { // should we expose a `normalTouchForce` constant somewhere (which would
// have a value of `1.0 / nativeTouch.maximumPossibleForce`)?
reactTouch[@"force"] = @(RCTZeroIfNaN(nativeTouch.force/nativeTouch.maximumPossibleForce)); if (RCTForceTouchAvailable()) {
reactTouch[@"force"] = @(RCTZeroIfNaN(nativeTouch.force / nativeTouch.maximumPossibleForce));
} }
} }

View File

@ -75,6 +75,9 @@ RCT_EXTERN UIApplication *__nullable RCTSharedApplication(void);
// or view controller, e.g. to present a modal view controller or alert. // or view controller, e.g. to present a modal view controller or alert.
RCT_EXTERN UIWindow *__nullable RCTKeyWindow(void); RCT_EXTERN UIWindow *__nullable RCTKeyWindow(void);
// Does this device support force touch (aka 3D Touch)?
RCT_EXTERN BOOL RCTForceTouchAvailable(void);
// Return a UIAlertView initialized with the given values // Return a UIAlertView initialized with the given values
// or nil if running in an app extension // or nil if running in an app extension
RCT_EXTERN UIAlertView *__nullable RCTAlertView(NSString *title, RCT_EXTERN UIAlertView *__nullable RCTAlertView(NSString *title,

View File

@ -434,6 +434,19 @@ UIWindow *__nullable RCTKeyWindow(void)
return RCTSharedApplication().keyWindow; return RCTSharedApplication().keyWindow;
} }
BOOL RCTForceTouchAvailable(void)
{
static BOOL forceSupported;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
forceSupported = [UITraitCollection class] &&
[UITraitCollection instancesRespondToSelector:@selector(forceTouchCapability)];
});
return forceSupported &&
(RCTKeyWindow() ?: [UIView new]).traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable;
}
UIAlertView *__nullable RCTAlertView(NSString *title, UIAlertView *__nullable RCTAlertView(NSString *title,
NSString *__nullable message, NSString *__nullable message,
id __nullable delegate, id __nullable delegate,
@ -475,7 +488,7 @@ id __nullable RCTNilIfNull(id __nullable value)
return value == (id)kCFNull ? nil : value; return value == (id)kCFNull ? nil : value;
} }
RCT_EXTERN double RCTZeroIfNaN(double value) double RCTZeroIfNaN(double value)
{ {
return isnan(value) || isinf(value) ? 0 : value; return isnan(value) || isinf(value) ? 0 : value;
} }

View File

@ -91,6 +91,11 @@ RCT_EXPORT_MODULE()
return @[]; return @[];
} }
- (NSDictionary<NSString *, id> *)constantsToExport
{
return @{@"forceTouchAvailable": @(RCTForceTouchAvailable())};
}
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(__unused RCTShadowView *)shadowView - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(__unused RCTShadowView *)shadowView
{ {
return nil; return nil;