Skip to content

Commit 8d10c0f

Browse files
committed
[Touchable] Add custom delay props and alter longPress implementation
1 parent c700f71 commit 8d10c0f

File tree

6 files changed

+183
-17
lines changed

6 files changed

+183
-17
lines changed

Examples/UIExplorer/TouchableExample.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ exports.examples = [
7575
render: function(): ReactElement {
7676
return <TouchableFeedbackEvents />;
7777
},
78+
}, {
79+
title: 'Touchable delay for events',
80+
description: '<Touchable*> components also accept delayPressIn, ' +
81+
'delayPressOut, and delayLongPress as props. These props impact the ' +
82+
'timing of feedback events.',
83+
render: function(): ReactElement {
84+
return <TouchableDelayEvents />;
85+
},
7886
}];
7987

8088
var TextOnPressBox = React.createClass({
@@ -148,6 +156,44 @@ var TouchableFeedbackEvents = React.createClass({
148156
},
149157
});
150158

159+
var TouchableDelayEvents = React.createClass({
160+
getInitialState: function() {
161+
return {
162+
eventLog: [],
163+
};
164+
},
165+
render: function() {
166+
return (
167+
<View>
168+
<View style={[styles.row, {justifyContent: 'center'}]}>
169+
<TouchableOpacity
170+
style={styles.wrapper}
171+
onPress={() => this._appendEvent('press')}
172+
delayPressIn={400}
173+
onPressIn={() => this._appendEvent('pressIn - 400ms delay')}
174+
delayPressOut={1000}
175+
onPressOut={() => this._appendEvent('pressOut - 1000ms delay')}
176+
delayLongPress={800}
177+
onLongPress={() => this._appendEvent('longPress - 800ms delay')}>
178+
<Text style={styles.button}>
179+
Press Me
180+
</Text>
181+
</TouchableOpacity>
182+
</View>
183+
<View style={styles.eventLogBox}>
184+
{this.state.eventLog.map((e, ii) => <Text key={ii}>{e}</Text>)}
185+
</View>
186+
</View>
187+
);
188+
},
189+
_appendEvent: function(eventName) {
190+
var limit = 6;
191+
var eventLog = this.state.eventLog.slice(0, limit - 1);
192+
eventLog.unshift(eventName);
193+
this.setState({eventLog});
194+
},
195+
});
196+
151197
var heartImage = {uri: 'https://pbs.twimg.com/media/BlXBfT3CQAA6cVZ.png:small'};
152198

153199
var styles = StyleSheet.create({

Libraries/Components/Touchable/TouchableHighlight.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var View = require('View');
2323

2424
var cloneWithProps = require('cloneWithProps');
2525
var ensureComponentIsNative = require('ensureComponentIsNative');
26+
var ensurePositiveDelayProps = require('ensurePositiveDelayProps');
2627
var keyOf = require('keyOf');
2728
var merge = require('merge');
2829
var onlyChild = require('onlyChild');
@@ -111,6 +112,7 @@ var TouchableHighlight = React.createClass({
111112
},
112113

113114
componentDidMount: function() {
115+
ensurePositiveDelayProps(this.props);
114116
ensureComponentIsNative(this.refs[CHILD_REF]);
115117
},
116118

@@ -119,6 +121,7 @@ var TouchableHighlight = React.createClass({
119121
},
120122

121123
componentWillReceiveProps: function(nextProps) {
124+
ensurePositiveDelayProps(nextProps);
122125
if (nextProps.activeOpacity !== this.props.activeOpacity ||
123126
nextProps.underlayColor !== this.props.underlayColor ||
124127
nextProps.style !== this.props.style) {
@@ -152,7 +155,8 @@ var TouchableHighlight = React.createClass({
152155
touchableHandlePress: function() {
153156
this.clearTimeout(this._hideTimeout);
154157
this._showUnderlay();
155-
this._hideTimeout = this.setTimeout(this._hideUnderlay, 100);
158+
this._hideTimeout = this.setTimeout(this._hideUnderlay,
159+
this.props.delayPressOut || 100);
156160
this.props.onPress && this.props.onPress();
157161
},
158162

@@ -164,6 +168,18 @@ var TouchableHighlight = React.createClass({
164168
return PRESS_RECT_OFFSET; // Always make sure to predeclare a constant!
165169
},
166170

171+
touchableGetHighlightDelayMS: function() {
172+
return this.props.delayPressIn;
173+
},
174+
175+
touchableGetLongPressDelayMS: function() {
176+
return this.props.delayLongPress;
177+
},
178+
179+
touchableGetPressOutDelayMS: function() {
180+
return this.props.delayPressOut;
181+
},
182+
167183
_showUnderlay: function() {
168184
this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps);
169185
this.refs[CHILD_REF].setNativeProps(this.state.activeProps);

Libraries/Components/Touchable/TouchableOpacity.js

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
var NativeMethodsMixin = require('NativeMethodsMixin');
1616
var POPAnimationMixin = require('POPAnimationMixin');
1717
var React = require('React');
18+
var TimerMixin = require('react-timer-mixin');
1819
var Touchable = require('Touchable');
1920
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
2021

2122
var cloneWithProps = require('cloneWithProps');
2223
var ensureComponentIsNative = require('ensureComponentIsNative');
24+
var ensurePositiveDelayProps = require('ensurePositiveDelayProps');
2325
var flattenStyle = require('flattenStyle');
2426
var keyOf = require('keyOf');
2527
var onlyChild = require('onlyChild');
@@ -50,7 +52,7 @@ var onlyChild = require('onlyChild');
5052
*/
5153

5254
var TouchableOpacity = React.createClass({
53-
mixins: [Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin],
55+
mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin],
5456

5557
propTypes: {
5658
...TouchableWithoutFeedback.propTypes,
@@ -72,13 +74,18 @@ var TouchableOpacity = React.createClass({
7274
},
7375

7476
componentDidMount: function() {
77+
ensurePositiveDelayProps(this.props);
7578
ensureComponentIsNative(this.refs[CHILD_REF]);
7679
},
7780

7881
componentDidUpdate: function() {
7982
ensureComponentIsNative(this.refs[CHILD_REF]);
8083
},
8184

85+
componentWillReceiveProps: function(nextProps) {
86+
ensurePositiveDelayProps(nextProps);
87+
},
88+
8289
setOpacityTo: function(value) {
8390
if (POPAnimationMixin) {
8491
// Reset with animation if POP is available
@@ -102,20 +109,24 @@ var TouchableOpacity = React.createClass({
102109
* defined on your component.
103110
*/
104111
touchableHandleActivePressIn: function() {
105-
this.refs[CHILD_REF].setNativeProps({
106-
opacity: this.props.activeOpacity
107-
});
112+
this.clearTimeout(this._hideTimeout);
113+
this._hideTimeout = null;
114+
this._opacityActive();
108115
this.props.onPressIn && this.props.onPressIn();
109116
},
110117

111118
touchableHandleActivePressOut: function() {
112-
var child = onlyChild(this.props.children);
113-
var childStyle = flattenStyle(child.props.style) || {};
114-
this.setOpacityTo(childStyle.opacity === undefined ? 1 : childStyle.opacity);
119+
if (!this._hideTimeout) {
120+
this._opacityInactive();
121+
}
115122
this.props.onPressOut && this.props.onPressOut();
116123
},
117124

118125
touchableHandlePress: function() {
126+
this.clearTimeout(this._hideTimeout);
127+
this._opacityActive();
128+
this._hideTimeout = this.setTimeout(this._opacityInactive,
129+
this.props.delayPressOut || 100);
119130
this.props.onPress && this.props.onPress();
120131
},
121132

@@ -128,7 +139,29 @@ var TouchableOpacity = React.createClass({
128139
},
129140

130141
touchableGetHighlightDelayMS: function() {
131-
return 0;
142+
return this.props.delayPressIn || 0;
143+
},
144+
145+
touchableGetLongPressDelayMS: function() {
146+
return this.props.delayLongPress === 0 ? 0 :
147+
this.props.delayLongPress || 500;
148+
},
149+
150+
touchableGetPressOutDelayMS: function() {
151+
return this.props.delayPressOut;
152+
},
153+
154+
_opacityActive: function() {
155+
this.setOpacityTo(this.props.activeOpacity);
156+
},
157+
158+
_opacityInactive: function() {
159+
this.clearTimeout(this._hideTimeout);
160+
this._hideTimeout = null;
161+
var child = onlyChild(this.props.children);
162+
var childStyle = flattenStyle(child.props.style) || {};
163+
this.setOpacityTo(childStyle.opacity === undefined ? 1 :
164+
childStyle.opacity);
132165
},
133166

134167
render: function() {

Libraries/Components/Touchable/TouchableWithoutFeedback.js

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
'use strict';
1313

1414
var React = require('React');
15+
var TimerMixin = require('react-timer-mixin');
1516
var Touchable = require('Touchable');
17+
var ensurePositiveDelayProps = require('ensurePositiveDelayProps');
1618
var onlyChild = require('onlyChild');
1719

1820
/**
@@ -31,7 +33,7 @@ type Event = Object;
3133
* one of the primary reason a "web" app doesn't feel "native".
3234
*/
3335
var TouchableWithoutFeedback = React.createClass({
34-
mixins: [Touchable.Mixin],
36+
mixins: [TimerMixin, Touchable.Mixin],
3537

3638
propTypes: {
3739
/**
@@ -42,12 +44,32 @@ var TouchableWithoutFeedback = React.createClass({
4244
onPressIn: React.PropTypes.func,
4345
onPressOut: React.PropTypes.func,
4446
onLongPress: React.PropTypes.func,
47+
/**
48+
* Delay in ms, from the start of the touch, before onPressIn is called.
49+
*/
50+
delayPressIn: React.PropTypes.number,
51+
/**
52+
* Delay in ms, from the release of the touch, before onPressOut is called.
53+
*/
54+
delayPressOut: React.PropTypes.number,
55+
/**
56+
* Delay in ms, from onPressIn, before onLongPress is called.
57+
*/
58+
delayLongPress: React.PropTypes.number,
4559
},
4660

4761
getInitialState: function() {
4862
return this.touchableGetInitialState();
4963
},
5064

65+
componentDidMount: function() {
66+
ensurePositiveDelayProps(this.props);
67+
},
68+
69+
componentWillReceiveProps: function(nextProps: Object) {
70+
ensurePositiveDelayProps(nextProps);
71+
},
72+
5173
/**
5274
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
5375
* defined on your component.
@@ -73,7 +95,16 @@ var TouchableWithoutFeedback = React.createClass({
7395
},
7496

7597
touchableGetHighlightDelayMS: function(): number {
76-
return 0;
98+
return this.props.delayPressIn || 0;
99+
},
100+
101+
touchableGetLongPressDelayMS: function(): number {
102+
return this.props.delayLongPress === 0 ? 0 :
103+
this.props.delayLongPress || 500;
104+
},
105+
106+
touchableGetPressOutDelayMS: function(): number {
107+
return this.props.delayPressOut || 0;
77108
},
78109

79110
render: function(): ReactElement {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ensurePositiveDelayProps
10+
* @flow
11+
*/
12+
'use strict';
13+
14+
var invariant = require('invariant');
15+
16+
var ensurePositiveDelayProps = function(props: any) {
17+
invariant(
18+
!(props.delayPressIn < 0 || props.delayPressOut < 0 ||
19+
props.delayLongPress < 0),
20+
'Touchable components cannot have negative delay properties'
21+
);
22+
};
23+
24+
module.exports = ensurePositiveDelayProps;

Libraries/vendor/react_contrib/interactions/Touchable/Touchable.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ var PRESS_EXPAND_PX = 20;
232232

233233
var LONG_PRESS_THRESHOLD = 500;
234234

235+
var LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS;
236+
235237
var LONG_PRESS_ALLOWED_MOVEMENT = 10;
236238

237239
// Default amount "active" region protrudes beyond box
@@ -276,7 +278,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10;
276278
* +
277279
* | RESPONDER_GRANT (HitRect)
278280
* v
279-
* +---------------------------+ DELAY +-------------------------+ T - DELAY +------------------------------+
281+
* +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+
280282
* |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN|
281283
* +---------------------------+ +-------------------------+ +------------------------------+
282284
* + ^ + ^ + ^
@@ -288,7 +290,7 @@ var LONG_PRESS_ALLOWED_MOVEMENT = 10;
288290
* |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT|
289291
* +----------------------------+ +--------------------------+ +-------------------------------+
290292
*
291-
* T - DELAY => LONG_PRESS_THRESHOLD - DELAY
293+
* T + DELAY => LONG_PRESS_DELAY_MS + DELAY
292294
*
293295
* Not drawn are the side effects of each transition. The most important side
294296
* effect is the `touchableHandlePress` abstract method invocation that occurs
@@ -348,12 +350,16 @@ var TouchableMixin = {
348350
// event to make sure it doesn't get reused in the event object pool.
349351
e.persist();
350352

353+
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
354+
this.pressOutDelayTimeout = null;
355+
351356
this.state.touchable.touchState = States.NOT_RESPONDER;
352357
this.state.touchable.responderID = dispatchID;
353358
this._receiveSignal(Signals.RESPONDER_GRANT, e);
354359
var delayMS =
355360
this.touchableGetHighlightDelayMS !== undefined ?
356-
this.touchableGetHighlightDelayMS() : HIGHLIGHT_DELAY_MS;
361+
Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS;
362+
delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS;
357363
if (delayMS !== 0) {
358364
this.touchableDelayTimeout = setTimeout(
359365
this._handleDelay.bind(this, e),
@@ -363,9 +369,13 @@ var TouchableMixin = {
363369
this._handleDelay(e);
364370
}
365371

372+
var longDelayMS =
373+
this.touchableGetLongPressDelayMS !== undefined ?
374+
Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS;
375+
longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS;
366376
this.longPressDelayTimeout = setTimeout(
367377
this._handleLongDelay.bind(this, e),
368-
LONG_PRESS_THRESHOLD - delayMS
378+
longDelayMS + delayMS
369379
);
370380
},
371381

@@ -632,8 +642,14 @@ var TouchableMixin = {
632642
if (newIsHighlight && !curIsHighlight) {
633643
this._savePressInLocation(e);
634644
this.touchableHandleActivePressIn && this.touchableHandleActivePressIn();
635-
} else if (!newIsHighlight && curIsHighlight) {
636-
this.touchableHandleActivePressOut && this.touchableHandleActivePressOut();
645+
} else if (!newIsHighlight && curIsHighlight && this.touchableHandleActivePressOut) {
646+
if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) {
647+
this.pressOutDelayTimeout = this.setTimeout(function() {
648+
this.touchableHandleActivePressOut();
649+
}, this.touchableGetPressOutDelayMS());
650+
} else {
651+
this.touchableHandleActivePressOut();
652+
}
637653
}
638654

639655
if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {

0 commit comments

Comments
 (0)