[Animated][BREAKING_CHANGE] Convert <TouchableOpacity> to Animated

Summary:
Because we don't want to integrate Animated inside of the core of React, we can only pass Animated.Value to styles of <Animated.View>. TouchableOpacity unfortunately used cloneElement. This means that we should have asked every single call site to replace their children to Animated.View. This isn't great.

The other solution is to stop using cloneElement and instead wrap the children inside of an <Animated.View>. This has many advantages:
- We no longer use cloneElement so we're no longer messing up with elements that are not our own.
- Refs are now working correctly for children elements
- No longer need to enforce that there's only one child and that this child is a native element

The downside is that we're introducing a <View> into the hierarchy. Sadly with CSS there is no way to have a View that doesn't affect layout. What we need to do is to remove the inner <View> and transfer all the styles to the TouchableOpacity. It is annoying but fortunately a pretty mechanical process.

I think that having a wrapper is the best solution. I will investigate to see if we can make wrappers on TouchableHighliht and TouchableWithoutFeedback as well.

**Upgrade Path:**

If the child is a View, move the style of the View to TouchableOpacity and remove the View itself.

```
<TouchableOpacity onPress={...}>
  <View style={...}>
    ...
  </View>
</TouchableOpacity>

-->

<TouchableOpacity onPress={...} style={...}>
  ...
</TouchableOpacity>
```

If the child is an Image or Text, on all the examples at Facebook it worked without any change. But it is a great idea to double check them anyway.
This commit is contained in:
Christopher Chedeau 2015-07-20 16:29:40 -07:00
parent 6213950334
commit 725053acfe
7 changed files with 109 additions and 133 deletions

View File

@ -48,19 +48,19 @@ var Thumb = React.createClass({
},
render: function() {
return (
<TouchableOpacity onPress={this._onPressThumb}>
<View style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
{this.state.dir === 'column' ?
<Text>
Oooo, look at this new text! So awesome it may just be crazy.
Let me keep typing here so it wraps at least one line.
</Text> :
<Text />
}
</View>
<TouchableOpacity
onPress={this._onPressThumb}
style={[styles.buttonContents, {flexDirection: this.state.dir}]}>
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
<Image style={styles.img} source={{uri: THUMB_URLS[this.state.thumbIndex]}} />
{this.state.dir === 'column' ?
<Text>
Oooo, look at this new text! So awesome it may just be crazy.
Let me keep typing here so it wraps at least one line.
</Text> :
<Text />
}
</TouchableOpacity>
);
}
@ -127,14 +127,12 @@ var ListViewPagingExample = React.createClass({
<View><Text style={styles.text}>1 Like</Text></View> :
null;
return (
<TouchableOpacity onPress={this._onPressHeader}>
<View style={styles.header}>
{headerLikeText}
<View>
<Text style={styles.text}>
Table Header (click me)
</Text>
</View>
<TouchableOpacity onPress={this._onPressHeader} style={styles.header}>
{headerLikeText}
<View>
<Text style={styles.text}>
Table Header (click me)
</Text>
</View>
</TouchableOpacity>
);

View File

@ -55,26 +55,24 @@ var BreadcrumbNavSample = React.createClass({
return (
<TouchableOpacity
onPress={() => navigator.push(_getRandomRoute())}>
<View>
<Text style={styles.titleText}>{route.title}</Text>
</View>
<Text style={styles.titleText}>{route.title}</Text>
</TouchableOpacity>
);
},
iconForRoute: function(route, navigator) {
return (
<TouchableOpacity onPress={() => {
navigator.popToRoute(route);
}}>
<View style={styles.crumbIconPlaceholder} />
</TouchableOpacity>
<TouchableOpacity
onPress={() => { navigator.popToRoute(route); }}
style={styles.crumbIconPlaceholder}
/>
);
},
separatorForRoute: function(route, navigator) {
return (
<TouchableOpacity onPress={navigator.pop}>
<View style={styles.crumbSeparatorPlaceholder} />
</TouchableOpacity>
<TouchableOpacity
onPress={navigator.pop}
style={styles.crumbSeparatorPlaceholder}
/>
);
}
};

View File

@ -51,12 +51,11 @@ var NavigationBarRouteMapper = {
var previousRoute = navState.routeStack[index - 1];
return (
<TouchableOpacity
onPress={() => navigator.pop()}>
<View style={styles.navBarLeftButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
{previousRoute.title}
</Text>
</View>
onPress={() => navigator.pop()}
style={styles.navBarLeftButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
{previousRoute.title}
</Text>
</TouchableOpacity>
);
},
@ -64,12 +63,11 @@ var NavigationBarRouteMapper = {
RightButton: function(route, navigator, index, navState) {
return (
<TouchableOpacity
onPress={() => navigator.push(newRandomRoute())}>
<View style={styles.navBarRightButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
Next
</Text>
</View>
onPress={() => navigator.push(newRandomRoute())}
style={styles.navBarRightButton}>
<Text style={[styles.navBarText, styles.navBarButtonText]}>
Next
</Text>
</TouchableOpacity>
);
},

View File

@ -59,12 +59,12 @@ var IntegrationTestsApp = React.createClass({
<View style={styles.separator} />
<ScrollView>
{TESTS.map((test) => [
<TouchableOpacity onPress={() => this.setState({test})}>
<View style={styles.row}>
<Text style={styles.testName}>
{test.displayName}
</Text>
</View>
<TouchableOpacity
onPress={() => this.setState({test})}
style={styles.row}>
<Text style={styles.testName}>
{test.displayName}
</Text>
</TouchableOpacity>,
<View style={styles.separator} />
])}

View File

@ -58,19 +58,19 @@ var WebViewExample = React.createClass({
return (
<View style={[styles.container]}>
<View style={[styles.addressBarRow]}>
<TouchableOpacity onPress={this.goBack}>
<View style={this.state.backButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'<'}
</Text>
</View>
<TouchableOpacity
onPress={this.goBack}
style={this.state.backButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'<'}
</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.goForward}>
<View style={this.state.forwardButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'>'}
</Text>
</View>
<TouchableOpacity
onPress={this.goForward}
style={this.state.forwardButtonEnabled ? styles.navButton : styles.disabledButton}>
<Text>
{'>'}
</Text>
</TouchableOpacity>
<TextInput
ref={TEXT_INPUT_REF}

View File

@ -12,19 +12,16 @@
// Note (avik): add @flow when Flow supports spread properties in propTypes
var Animated = require('Animated');
var NativeMethodsMixin = require('NativeMethodsMixin');
var POPAnimationMixin = require('POPAnimationMixin');
var React = require('React');
var TimerMixin = require('react-timer-mixin');
var Touchable = require('Touchable');
var TouchableWithoutFeedback = require('TouchableWithoutFeedback');
var cloneWithProps = require('cloneWithProps');
var ensureComponentIsNative = require('ensureComponentIsNative');
var ensurePositiveDelayProps = require('ensurePositiveDelayProps');
var flattenStyle = require('flattenStyle');
var keyOf = require('keyOf');
var onlyChild = require('onlyChild');
/**
* A wrapper for making views respond properly to touches.
@ -49,7 +46,7 @@ var onlyChild = require('onlyChild');
*/
var TouchableOpacity = React.createClass({
mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin, POPAnimationMixin],
mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin],
propTypes: {
...TouchableWithoutFeedback.propTypes,
@ -67,16 +64,17 @@ var TouchableOpacity = React.createClass({
},
getInitialState: function() {
return this.touchableGetInitialState();
return {
...this.touchableGetInitialState(),
anim: new Animated.Value(1),
};
},
componentDidMount: function() {
ensurePositiveDelayProps(this.props);
ensureComponentIsNative(this.refs[CHILD_REF]);
},
componentDidUpdate: function() {
ensureComponentIsNative(this.refs[CHILD_REF]);
},
componentWillReceiveProps: function(nextProps) {
@ -84,22 +82,10 @@ var TouchableOpacity = React.createClass({
},
setOpacityTo: function(value) {
if (POPAnimationMixin) {
// Reset with animation if POP is available
this.stopAllAnimations();
var anim = {
type: this.AnimationTypes.linear,
property: this.AnimationProperties.opacity,
duration: 0.15,
toValue: value,
};
this.startAnimation(CHILD_REF, anim);
} else {
// Reset immediately if POP is unavailable
this.refs[CHILD_REF].setNativeProps({
opacity: value
});
}
Animated.timing(
this.state.anim,
{toValue: value, duration: 150}
).start();
},
/**
@ -158,25 +144,27 @@ var TouchableOpacity = React.createClass({
_opacityInactive: function() {
this.clearTimeout(this._hideTimeout);
this._hideTimeout = null;
var child = onlyChild(this.props.children);
var childStyle = flattenStyle(child.props.style) || {};
var childStyle = flattenStyle(this.props.style) || {};
this.setOpacityTo(
childStyle.opacity === undefined ? 1 : childStyle.opacity
);
},
render: function() {
return cloneWithProps(onlyChild(this.props.children), {
ref: CHILD_REF,
accessible: true,
testID: this.props.testID,
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
onResponderGrant: this.touchableHandleResponderGrant,
onResponderMove: this.touchableHandleResponderMove,
onResponderRelease: this.touchableHandleResponderRelease,
onResponderTerminate: this.touchableHandleResponderTerminate,
});
return (
<Animated.View
accessible={true}
style={[this.props.style, {opacity: this.state.anim}]}
testID={this.props.testID}
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
onResponderGrant={this.touchableHandleResponderGrant}
onResponderMove={this.touchableHandleResponderMove}
onResponderRelease={this.touchableHandleResponderRelease}
onResponderTerminate={this.touchableHandleResponderTerminate}>
{this.props.children}
</Animated.View>
);
},
});
@ -188,6 +176,5 @@ var TouchableOpacity = React.createClass({
*/
var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
var CHILD_REF = keyOf({childRef: null});
module.exports = TouchableOpacity;

View File

@ -174,15 +174,13 @@ var WarningRow = React.createClass({
{...this.panGesture.panHandlers}>
<TouchableOpacity
onPress={this.props.onOpened}>
<View>
<Text
style={styles.warningText}
numberOfLines={2}
ref={text => { this.text = text; }}>
{countText}
{this.props.warning}
</Text>
</View>
<Text
style={styles.warningText}
numberOfLines={2}
ref={text => { this.text = text; }}>
{countText}
{this.props.warning}
</Text>
</TouchableOpacity>
<View
ref={closeButton => { this.closeButton = closeButton; }}
@ -212,30 +210,27 @@ var WarningBoxOpened = React.createClass({
return (
<TouchableOpacity
activeOpacity={0.9}
onPress={this.props.onClose}>
<View style={styles.yellowBox}>
<Text style={styles.yellowBoxText}>
{countText}
{this.props.warning}
</Text>
<View style={styles.yellowBoxButtons}>
<View style={styles.yellowBoxButton}>
<TouchableOpacity
onPress={this.props.onDismissed}>
<Text style={styles.yellowBoxButtonText}>
Dismiss
</Text>
</TouchableOpacity>
</View>
<View style={styles.yellowBoxButton}>
<TouchableOpacity
onPress={this.props.onIgnored}>
<Text style={styles.yellowBoxButtonText}>
Ignore
</Text>
</TouchableOpacity>
</View>
</View>
onPress={this.props.onClose}
style={styles.yellowBox}>
<Text style={styles.yellowBoxText}>
{countText}
{this.props.warning}
</Text>
<View style={styles.yellowBoxButtons}>
<TouchableOpacity
onPress={this.props.onDismissed}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Dismiss
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.props.onIgnored}
style={styles.yellowBoxButton}>
<Text style={styles.yellowBoxButtonText}>
Ignore
</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
);