Permalink
Browse files

[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.
  • Loading branch information...
vjeux committed Jul 20, 2015
1 parent 6213950 commit 725053acfeba4c7f2a21ac47ae8100588a710476
@@ -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>
);
@@ -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}
+ />
);
}
};
@@ -51,25 +51,23 @@ 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>
);
},
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>
);
},
@@ -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} />
])}
@@ -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}
@@ -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,39 +64,28 @@ 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) {
ensurePositiveDelayProps(nextProps);
},
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;
@@ -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>
);

0 comments on commit 725053a

Please sign in to comment.