From f685878938b59f858dc65b8e5e873045a25ccf79 Mon Sep 17 00:00:00 2001 From: Nick Lockwood Date: Wed, 27 Jan 2016 09:04:14 -0800 Subject: [PATCH] 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 --- Examples/UIExplorer/TouchableExample.js | 104 +++++++++++++++----- Libraries/Components/Touchable/Touchable.js | 10 +- Libraries/Components/View/View.js | 54 ++++++---- React/Base/RCTTouchHandler.m | 13 ++- React/Base/RCTUtils.h | 3 + React/Base/RCTUtils.m | 15 ++- React/Views/RCTViewManager.m | 5 + 7 files changed, 144 insertions(+), 60 deletions(-) diff --git a/Examples/UIExplorer/TouchableExample.js b/Examples/UIExplorer/TouchableExample.js index 7b0bf2c0b..dd3b2c9c8 100644 --- a/Examples/UIExplorer/TouchableExample.js +++ b/Examples/UIExplorer/TouchableExample.js @@ -23,6 +23,7 @@ var { Text, TouchableHighlight, TouchableOpacity, + UIManager, View, } = React; @@ -85,6 +86,13 @@ exports.examples = [ render: function(): ReactElement { return ; }, +}, { + 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 ; + }, + platform: 'ios', }]; var TextOnPressBox = React.createClass({ @@ -133,18 +141,18 @@ var TouchableFeedbackEvents = React.createClass({ return ( - this._appendEvent('press')} - onPressIn={() => this._appendEvent('pressIn')} - onPressOut={() => this._appendEvent('pressOut')} - onLongPress={() => this._appendEvent('longPress')}> - - Press Me - - - + this._appendEvent('press')} + onPressIn={() => this._appendEvent('pressIn')} + onPressOut={() => this._appendEvent('pressOut')} + onLongPress={() => this._appendEvent('longPress')}> + + Press Me + + + {this.state.eventLog.map((e, ii) => {e})} @@ -169,21 +177,21 @@ var TouchableDelayEvents = React.createClass({ return ( - 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')}> - - Press Me - - - + 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')}> + + Press Me + + + {this.state.eventLog.map((e, ii) => {e})} @@ -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 ( + + + {this._renderConsoleText()} + + + true} + onResponderMove={(event) => this.setState({force: event.nativeEvent.force})} + onResponderRelease={(event) => this.setState({force: 0})}> + + Press Me + + + + + ); + }, +}); + 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', diff --git a/Libraries/Components/Touchable/Touchable.js b/Libraries/Components/Touchable/Touchable.js index e0ea8850b..9f457c5c0 100644 --- a/Libraries/Components/Touchable/Touchable.js +++ b/Libraries/Components/Touchable/Touchable.js @@ -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 ( - *
- *
+ * * 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. - *
- *
+ * + * * ); * * - You may set up your own handlers for each of these events, so long as you diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index beaffb75c..a7f31f9a0 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -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; diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index c0f6b6f42..f3050cfe9 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -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)); } } diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index 8f7a4858b..c751dabbd 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -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, diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index f49e117e2..68e3a15e5 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -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; } diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index bbbca7da1..68980e0bf 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -91,6 +91,11 @@ RCT_EXPORT_MODULE() return @[]; } +- (NSDictionary *)constantsToExport +{ + return @{@"forceTouchAvailable": @(RCTForceTouchAvailable())}; +} + - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowView:(__unused RCTShadowView *)shadowView { return nil;