mirror of
https://github.com/status-im/react-native.git
synced 2025-02-05 06:04:15 +00:00
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:
parent
21a4c6e853
commit
f685878938
@ -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',
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user