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,
TouchableHighlight,
TouchableOpacity,
UIManager,
View,
} = React;
@ -85,6 +86,13 @@ exports.examples = [
render: function(): ReactElement {
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({
@ -133,18 +141,18 @@ var TouchableFeedbackEvents = React.createClass({
return (
<View testID="touchable_feedback_events">
<View style={[styles.row, {justifyContent: 'center'}]}>
<TouchableOpacity
style={styles.wrapper}
testID="touchable_feedback_events_button"
onPress={() => this._appendEvent('press')}
onPressIn={() => this._appendEvent('pressIn')}
onPressOut={() => this._appendEvent('pressOut')}
onLongPress={() => this._appendEvent('longPress')}>
<Text style={styles.button}>
Press Me
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.wrapper}
testID="touchable_feedback_events_button"
onPress={() => this._appendEvent('press')}
onPressIn={() => this._appendEvent('pressIn')}
onPressOut={() => this._appendEvent('pressOut')}
onLongPress={() => this._appendEvent('longPress')}>
<Text style={styles.button}>
Press Me
</Text>
</TouchableOpacity>
</View>
<View testID="touchable_feedback_events_console" style={styles.eventLogBox}>
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
</View>
@ -169,21 +177,21 @@ var TouchableDelayEvents = React.createClass({
return (
<View testID="touchable_delay_events">
<View style={[styles.row, {justifyContent: 'center'}]}>
<TouchableOpacity
style={styles.wrapper}
testID="touchable_delay_events_button"
onPress={() => this._appendEvent('press')}
delayPressIn={400}
onPressIn={() => this._appendEvent('pressIn - 400ms delay')}
delayPressOut={1000}
onPressOut={() => this._appendEvent('pressOut - 1000ms delay')}
delayLongPress={800}
onLongPress={() => this._appendEvent('longPress - 800ms delay')}>
<Text style={styles.button}>
Press Me
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.wrapper}
testID="touchable_delay_events_button"
onPress={() => this._appendEvent('press')}
delayPressIn={400}
onPressIn={() => this._appendEvent('pressIn - 400ms delay')}
delayPressOut={1000}
onPressOut={() => this._appendEvent('pressOut - 1000ms delay')}
delayLongPress={800}
onLongPress={() => this._appendEvent('longPress - 800ms delay')}>
<Text style={styles.button}>
Press Me
</Text>
</TouchableOpacity>
</View>
<View style={styles.eventLogBox} testID="touchable_delay_events_console">
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
</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 styles = StyleSheet.create({
@ -241,6 +283,14 @@ var styles = StyleSheet.create({
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9',
},
forceTouchBox: {
padding: 10,
margin: 10,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9',
alignItems: 'center',
},
textBlock: {
fontWeight: '500',
color: 'blue',

View File

@ -49,24 +49,24 @@ var queryLayoutByID = require('queryLayoutByID');
*
* - Choose the rendered component who's touches should start the interactive
* 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:
*
* // In render function:
* return (
* <div
* <View
* onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
* onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
* onResponderGrant={this.touchableHandleResponderGrant}
* onResponderMove={this.touchableHandleResponderMove}
* onResponderRelease={this.touchableHandleResponderRelease}
* onResponderTerminate={this.touchableHandleResponderTerminate}>
* <div>
* <View>
* Even though the hit detection/interactions are triggered by the
* wrapping (typically larger) node, we usually end up implementing
* custom logic that highlights this inner one.
* </div>
* </div>
* </View>
* </View>
* );
*
* - You may set up your own handlers for each of these events, so long as you

View File

@ -11,20 +11,20 @@
*/
'use strict';
var NativeMethodsMixin = require('NativeMethodsMixin');
var PropTypes = require('ReactPropTypes');
var React = require('React');
var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes');
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
var StyleSheetPropType = require('StyleSheetPropType');
var UIManager = require('UIManager');
var ViewStylePropTypes = require('ViewStylePropTypes');
const NativeMethodsMixin = require('NativeMethodsMixin');
const PropTypes = require('ReactPropTypes');
const React = require('React');
const ReactNativeStyleAttributes = require('ReactNativeStyleAttributes');
const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
const StyleSheetPropType = require('StyleSheetPropType');
const UIManager = require('UIManager');
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',
'button',
'link',
@ -44,13 +44,26 @@ var AccessibilityTraits = [
'pageTurn',
];
var AccessibilityComponentType = [
const AccessibilityComponentType = [
'none',
'button',
'radiobutton_checked',
'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
* 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
* performance, although inline styles are also supported.
*/
var View = React.createClass({
const View = React.createClass({
mixins: [NativeMethodsMixin],
/**
@ -84,8 +97,7 @@ var View = React.createClass({
},
statics: {
AccessibilityTraits,
AccessibilityComponentType,
...statics,
},
propTypes: {
@ -320,16 +332,16 @@ var View = React.createClass({
},
});
var RCTView = requireNativeComponent('RCTView', View, {
const RCTView = requireNativeComponent('RCTView', View, {
nativeOnly: {
nativeBackgroundAndroid: true,
}
});
if (__DEV__) {
var viewConfig = UIManager.viewConfigs && UIManager.viewConfigs.RCTView || {};
for (var prop in viewConfig.nativeProps) {
var viewAny: any = View; // Appease flow
const viewConfig = UIManager.viewConfigs && UIManager.viewConfigs.RCTView || {};
for (const prop in viewConfig.nativeProps) {
const viewAny: any = View; // Appease flow
if (!viewAny.propTypes[prop] && !ReactNativeStyleAttributes[prop]) {
throw new Error(
'View is missing propType for native prop `' + prop + '`'
@ -338,9 +350,11 @@ if (__DEV__) {
}
}
var ViewToExport = RCTView;
let ViewToExport = RCTView;
if (__DEV__) {
ViewToExport = View;
} else {
Object.assign(RCTView, statics);
}
module.exports = ViewToExport;

View File

@ -108,11 +108,9 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
}
// Create touch
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:11];
NSMutableDictionary *reactTouch = [[NSMutableDictionary alloc] initWithCapacity:RCTMaxTouches];
reactTouch[@"target"] = reactTag;
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
[_touchViews addObject:targetView];
@ -151,10 +149,11 @@ typedef NS_ENUM(NSInteger, RCTTouchEventType) {
reactTouch[@"locationY"] = @(touchViewLocation.y);
reactTouch[@"timestamp"] = @(nativeTouch.timestamp * 1000); // in ms, for JS
if ([nativeTouch.view respondsToSelector:@selector(traitCollection)] &&
[nativeTouch.view.traitCollection respondsToSelector:@selector(forceTouchCapability)]) {
reactTouch[@"force"] = @(RCTZeroIfNaN(nativeTouch.force/nativeTouch.maximumPossibleForce));
// TODO: force for a 'normal' touch is usually 1.0;
// should we expose a `normalTouchForce` constant somewhere (which would
// have a value of `1.0 / 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.
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
// or nil if running in an app extension
RCT_EXTERN UIAlertView *__nullable RCTAlertView(NSString *title,

View File

@ -434,6 +434,19 @@ UIWindow *__nullable RCTKeyWindow(void)
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,
NSString *__nullable message,
id __nullable delegate,
@ -475,7 +488,7 @@ id __nullable RCTNilIfNull(id __nullable value)
return value == (id)kCFNull ? nil : value;
}
RCT_EXTERN double RCTZeroIfNaN(double value)
double RCTZeroIfNaN(double value)
{
return isnan(value) || isinf(value) ? 0 : value;
}

View File

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